Author: Bob Nystrom
Status: Accepted
Version 1.21 (see CHANGELOG at end)
When you want to bundle multiple objects into a single value, Dart gives you a couple of options. You can define a class with fields for the values. This works well when you also have meaningful behavior to attach to the data. But it's quite verbose and it means any other code using this bundle of data is now also coupled to that particular class definition.
You can wrap them in a collection like a list, map, or set. This is lightweight
and avoids bringing in any coupling other than the Dart core library. But it
does not work well with the static type system. If you want to bundle a number
and a string together, the best you can do is a List<Object>
and then the type
system has lost track of how many elements there are and what their individual
types are.
You've probably noticed this if you've ever used Future.wait()
to wait on a
couple of futures of different types. You end up having to cast the results back
out since the type system no longer knows which element in the returned list has
which type.
This proposal, part of the larger "tuples, records, and pattern matching" family of features, adds records to Dart. Records are an anonymous immutable aggregate type. Like lists and maps, they let you combine several values into a single new object. Unlike other collection types, records are fixed-sized, heterogeneous, and typed. Each element in a record may have a different type and the static type system tracks them separately.
Unlike classes, records are structurally typed. You do not have to declare a record type and give it a name. If two unrelated libraries create records with the same set of fields, the type system understands that those records are the same type even though the libraries are not coupled to each other.
Many languages, especially those with a static functional heritage, have tuple or product types:
var tuple = ("first", 2, true);
A tuple is an ordered list of unnamed positional fields. These languages also often have record types. In a record, the fields are unordered and identified by name instead:
var record = (number: 123, name: "Main", type: "Street");
In Dart, we merge both of these into a single construct, called a record. A record has a series of fields, which may be named or positional.
var record = (1, a: 2, 3, b: 4);
The expression syntax looks much like an argument list to a function call. A
record expression like the above examples produces a record value. This is a
first-class object, literally a subtype of Object. Its fields cannot be
modified, but may contain references to mutable objects. It implements
hashCode
and ==
structurally based on its fields to provide value-type
semantics.
A record may have only positional fields, only named fields, both, or none at all.
Once a record has been created, its fields can be accessed using getters.
Every named field exposes a getter with the same name, and positional fields
expose getters named $1
, $2
, etc.:
var record = ("ape", a: "bat", "cat", b: "dog");
print(record.$1); // Prints "ape".
print(record.a); // Prints "bat".
print(record.$2); // Prints "cat".
print(record.b); // Prints "dog".
These primitive types are added to dart:core
:
The type Record
refers to a built-in class defined in dart:core
. It has no
instance members except those inherited from Object
and exposes no
constructors. It can't be constructed, extended, mixed in, or implemented by
user-defined classes.
All record types are a subtype of this class. This is similar to how the
Function
class is the superclass for all function types.
A record is created using a record expression, like the examples above. The grammar is:
literal ::= record
| // Existing literal productions...
record ::= 'const'? '(' recordField ( ',' recordField )* ','? ')'
recordField ::= (identifier ':' )? expression
This is identical to the grammar for a function call argument list (with an
optional const
at the beginning). There are a couple of syntactic restrictions
not captured by the grammar. It is a compile-time error if a record has any of:
-
The same field name more than once.
-
Only one positional field and no trailing comma.
-
No fields and a trailing comma. The expression
(,)
isn't allowed. -
A field named
hashCode
,runtimeType
,noSuchMethod
, ortoString
. -
A field name that starts with an underscore.
-
A field name that collides with the synthesized getter name of a positional field. For example:
('pos', $1: 'named')
since the named field "$1" collides with the getter for the first positional field.
In order to avoid ambiguity with parenthesized expressions, a record with only a single positional field must have a trailing comma:
var number = (123); // The number 123.
var record = (123,); // A record containing the number 123.
The expression ()
refers to the constant empty record with no fields.
In the type system, each record has a corresponding record type. A record type looks similar to a function type's parameter list. The type is surrounded by parentheses and may contain comma-separated positional fields:
(int, String name, bool) triple;
Each field is a type annotation and an optional name which isn't meaningful but is useful for documentation purposes.
Named fields go inside a brace-delimited section of type and name pairs:
({int n, String s}) pair;
A record type may have both positional and named fields:
(bool, num, {int n, String s}) quad;
The grammar is:
// Existing rules:
typeNotFunction ::= 'void' // Existing production.
| recordType '?'? // New production.
| typeNotVoidNotFunction // Existing production.
typeNotVoid ::= functionType '?'? // Existing production.
| recordType '?'? // New production.
| typeNotVoidNotFunction // Existing production.
// New rules:
recordType ::= '(' recordTypeFields ',' recordTypeNamedFields ')'
| '(' recordTypeFields ','? ')'
| '(' recordTypeNamedFields? ')'
recordTypeFields ::= recordTypeField ( ',' recordTypeField )*
recordTypeField ::= metadata type identifier?
recordTypeNamedFields ::= '{' recordTypeNamedField
( ',' recordTypeNamedField )* ','? '}'
recordTypeNamedField ::= metadata type identifier
The grammar is exactly the same as parameterTypeList
in function types but
without required
, and optional positional parameters since those don't apply
to record types.
The type ()
is the type of an empty record with no fields.
It is a compile-time error if a record type has any of:
-
The same field name more than once. This is true even if one or both of the colliding fields is positional. We could permit collisions with positional field names since they are only used for documentation, but we disallow it because it's confusing and not useful.
-
Only one positional field and no trailing comma. This isn't ambiguous, since there are no parenthesized type expressions in Dart. But prohibiting this is symmetric with record expressions and leaves the potential for later support for parentheses for grouping in type expressions.
-
A field named
hashCode
,runtimeType
,noSuchMethod
, ortoString
. -
A field name that starts with an underscore.
-
A field name that collides with the synthesized getter name of a positional field. For example:
(int, $1: int)
since the named field "$1" collides with the getter for the first positional field.
There is no record type literal syntax that can be used as an expression, since it would be ambiguous with other existing syntax:
var t = (int, String);
This is a record expression containing two type literals, int
and String
,
not a type literal for a record type.
Consider:
void foo() {
try {
;
} on Bar {
;
}
on(a, b) {;} // <--
}
Before, the marked line could only be declaring a local function named on
.
With record types, it could be a second on
clause for the try
statement
whose matched type is the record type (a, b)
.
*We could disambiguate this by saying we treat the code as an on
clause only
if it could be successfully parsed as one. In other words, if the thing after
on
could only be a parameter list and not a record type, we would continue to
parse it as a local function declaration. But that adds significant complexity
and lookahead to the parser. Even when not ambiguous, it is certainly
confusing to have a local function named on
immediately following a try
block.
Whenever on
appears after a try
block or after a preceding on
clause on a
try block, we unconditionally parse it as an on
clause and not a local
function. This may yield a syntax error if the code after on
is not a on
clause (but would be a valid function declaration). In other words, you can't
have a local function named on
with no return type immediately following a
try
block.
This is technically a breaking change, but is unlikely to affect any code in the
wild. Given that, and that our parser is currently not aware of language
versioning, we make this grammar change unconditionally when parsing a Dart
library targeting any language version. In a Dart library whose language version
is prior to the version that records ship in, an on
keyword followed by a
parenthesized type will be parsed as a record type, which will then be reported
as an error since record types are not supported in that language version.
A metadata annotation may or may not have an argument list following it. A variable declaration may omit a preceding type annotation. Likewise, a function declaration may omit a preceding return type. This combination of syntax where an optional trailing element is followed by syntax with an optional preceding element can lead to ambiguity. In particular:
@metadata (a, b) function() {}
This could be a metadata annotation @metadata(a, b)
associated with a function
declaration with no return type. Or it could be a metadata annotation
@metadata
associated with a function whose return type is the record type (a, b)
.
In practice, idiomatically written code is clear thanks to whitespace:
@metadata(a, b) function() {}
@metadata (a, b) function() {}
The former applies (a, b)
to the metadata annotation and the latter is a
return type. We disambiguate in the same way, by making whitespace after a
metadata annotation name significant. Change the grammar to:
metadatum ::= identifier // Existing rule.
| qualifiedName // Existing rule.
| constructorDesignation NO_SPACE arguments // Changed.
The NO_SPACE
lexical rule matches when there are no whitespace characters or
comments (according to the existing WHITESPACE
and COMMENT
lexical rules)
between the constructorDesignation
and arguments
. In other words, for an
argument list to be part of the metadata annotation, the (
must occur
immediately after the last character in the constructorDesignation
. The last
character in constructorDesignation
may be an identifier or the >
in a
type argument list.
// These are parsed as argument lists to the annotation:
@metadata(x, y) a;
@metadata<T>(x, y) a;
@metadata <T>(x, y) a;
// These are parsed as record variable types:
@metadata (x, y) a;
@metadata
(x, y) a;
@metadata/* comment */(x, y) a;
@metadata // Comment.
(x,) a;
Note that the NO_SPACE
rule is applied unconditionally, even when the metadata
annotation appears in a context where no ambiguity with record types is
possible, as in:
@metadata (x, y)
class C {}
This example has a syntax error because the (x, y)
is not parsed as arguments
to the metadata and can't be parsed as anything else either.
Another interesting case is:
@metadata<T> (x, y) a;
This is a syntax error because the <T>
means there must be an argument list
after it, but the NO_SPACE
in metadatum
prevents it from being parsed as
such and the result is an error. We could ignore whitespace after the >
by
tweaking the grammar, but we choose to require NO_SPACE
even here since
@metadata<T>
appears to be a generic instantiation and could potentially be
valid syntax in the future.
Breaking change: Existing metadata annotations with whitespace before their argument lists will no longer parse correctly. In a corpus of 18,672,247 lines of code containing 409,825 metadata annotations, 46,245 had argument lists and none of those had whitespace before the argument list. Note that this analysis only captures code that has been committed. Code being written may be less well formatted, but we expect problems from this to be rare.
Because this change does not seem to break a significant fraction of code in the wild and our parser is currently not aware of language versioning, we make this grammar change unconditionally when parsing a Dart library targeting any language version.
We define shape to mean the number of positional fields (the record's arity) and the set of names of its named fields. Record types are structural, not nominal. Records produced in unrelated libraries have the exact same static type if they have the same shape and their corresponding fields have the same types.
The order of named fields is not significant. The record types {int a, int b}
and {int b, int a}
are identical to the type system and the runtime. (Tools
may or may not display them to users in a canonical form similar to how they
handle function typedefs.)
Positional fields are not merely syntactic sugar for fields named $1
, $2
,
etc. The records ('a', 'b')
and ($1: 'a', $2: 'b')
expose the same
members, but have different shapes according to the type system.
A record type declares all of the members defined on Object
. It also exposes
getters for each named field where the name of the getter is the field's name
and the getter's type is the field's type. For each positional field, it exposes
a getter whose whose type is the type of the field and whose name is $n
where
n
is the field's 1
-based index in the positional fields.
For example, the record expression (1.2, name: 's', true, count: 3)
has a
record type whose signature is like:
class extends Record {
double get $1;
String get name;
bool get $2;
int get count;
}
Subtyping for record types has been incorporated into the main subtyping specification here. Briefly:
The class Record
is a subtype of Object
and dynamic
and a supertype of
Never
. All record types are subtypes of Record
, and supertypes of Never
.
A record type A
is a subtype of record type B
iff they have same shape and
the types of all fields of A
are subtypes of the corresponding field types of
B
. In type system lingo, this means record types are "covariant" or have
"depth subtyping". Record types with different shapes are not subtypes. There is
no "row polymorphism" or "width subtyping".
Bounds computations for record types have been incorporated into the main specification here. Briefly:
If two record types have the same shape, their least upper bound is a new record type of the same shape where each field's type is the least upper bound of the corresponding field in the original types.
(num, String) a = (1.2, "s");
(int, Object) b = (2, true);
var c = cond ? a : b; // c has type `(num, Object)`.
Likewise, the greatest lower bound of two record types with the same shape is the greatest lower bound of their component fields:
a((num, String) record) {}
b((int, Object) record) {}
var c = cond ? a : b; // c has type `Function((int, String))`.
The least upper bound of two record types with different shapes is Record
.
(num, String) a = (1.2, "s");
(num, String, bool) b = (2, "s", true);
var c = cond ? a : b; // c has type `Record`.
The greatest lower bound of records with different shapes is Never
.
In order to determine how a combined member signature is computed when one
or more types in the signature is a record type, we need to extend the
definition of the type function NNBD_TOP_MERGE
when it is applied to a
record type:
NNBD_TOP_MERGE
is only defined when it is applied to two types with the
same structure, in particular: It is only defined on two record types when
they have the same shape.
When it is applied to two record types with the same shape, the result
is a record type with the same shape again, where each field type is
obtained by applying NNBD_TOP_MERGE
recursively.
For example:
NNBD_TOP_MERGE((int, {dynamic x}), (int, {void x})) == (int, {Object? x})
Every record literal has a static type, which is associated with it via inference (there is no syntax for explicitly associating a specific static type with a record literal). As with other constructs, we define type inference for record expressions with respect to a context type schema which is determined by the surrounding context of the inferred expression. Unlike nominal classes (but like function literals) we choose to infer the most specific possible type for record literals, even if that type is more precise than the type specified by the context type schema. This choice (as with function literals) reflects the fact that record types (like function types in Dart) are soundly variant: that is, the subtyping is properly covariant and requires no runtime checking.
For convenience, we generally write function types with all named parameters in an unspecified canonical order, and similarly for the named fields of record types. In all cases unless otherwise specifically called out, order of named parameters and fields is semantically irrelevant: any two types with the same named parameters (named fields, respectively) are considered the same type.
Similarly, function and method invocations with named arguments and records with named field entries are written with their named entries in an unspecified canonical order and position. Unless otherwise called out, position of named entries is semantically irrelevant, and all invocations and record literals with the same named entries (possibly in different orders or locations) and the same positional entries are considered equivalent.
Given a type schema K
and a record expression E
of the general form (e1, ..., en, d1 : e{n+1}, ..., dm : e{n+m})
inference proceeds as follows.
If K
is a record type schema of the form (K1, ..., Kn, {d1 : K{n+1}, ...., dm : K{n+m}})
then:
- Each
ei
is inferred with context type schemaKi
to have typeSi
- Let
Ri
be the greatest closure ofKi
- If
Si
is a subtype ofRi
then letTi
beSi
- Otherwise, if
Si
isdynamic
, then we insert an implicit cast onei
toRi
, and letTi
beRi
- Otherwise, if
Si
is coercible toRi
(via some sequence of call method tearoff or implicit generic instantiation coercions), then we insert the appropriate implicit coercion(s) onei
. LetTi
be the type of the resulting coerced value (which must be a subtype ofRi
, possibly proper). - Otherwise, let
Ti
beSi
.
- Let
- The type of
E
is(T1, ..., Tn, {d1 : T{n+1}, ...., dm : T{n+m}})
If K
is any other type schema:
- Each
ei
is inferred with context type schema_
to have typeTi
- The type of
E
is(T1, ..., Tn, {d1 : T{n+1}, ...., dm : T{n+m}})
As noted above, contrary to the practice for runtime checked covariant nominal types, we do not prefer the context type over the more precise upwards type. The following example illustrates this:
// No static error.
// Inferred type of the record is (int, double)
(num, num) r = (3, 3.5)..$1.isEven;
Also note that implicit casts and other coercions are considered to be applied as part of inference, hence:
class Callable {
void call(num x) {}
}
T id<T>(T x) => x;
// No static error.
// Inferred type of the record is:
// (int, double, int Function(int), void Function(num))
var c = Callable();
dynamic d = 3;
(num, double, int Function(int), void Function(num)) r = (d, 3, id, c);
and the record initialization in the last line above is implicitly coerced to be the equivalent of:
(num, double, int Function(int), void Function(num)) r =
(d as num, 3.0, id<int>, c.call);
See issue 2488 for some of the background discussion on the choices specified in this section.
Record expressions can be constant and potentially constant expressions. A record expression is a compile-time constant expression if and only if all of its field expressions are compile-time constant expressions.
*This is true whether the expression occurs in a constant context or not, which means that a record expression can be used directly as a parameter default value if its record field expressions are constant expressions, as in:
void someFunction({(int, int) x = (1, 2)}) => ...`
A record expression is a potentially constant expression if and only if all its field expressions are potentially constant or constant expressions. This means that a record expression can be used in the initializer list of a constant non-redirecting generative constructor, and can depend on constructor parameters.
Constant object instantiations (i.e. const constructor calls and const collection literals) create deeply immutable and canonicalized objects. Records are always unmodifiable. If a record's field values are also deeply immutable (which all constant values are), then the record is also deeply immutable. It's meaningless to consider whether record constants are canonicalized, since records do not have a persistent identity.
Therefore, there is no need for a const (1, 2)
syntax to force a record to be
a constant like there is for constructor calls. Any record expression with field
values that are constant is indistinguishable from a similar expression created
in a constant context, since identity cannot be used as a distinguishing trait.
The current specification relies on identical()
to decide when to canonicalize
constant object creation expressions. Since identical()
is not useful for
records (see below), we update that:
Define two Dart values, a and b, to be structurally equivalent as follows:
-
If a and b are both records, and they have the same shape, and for each field f of that shape, the records' values of that field, af and bf are structurally equivalent, then a and b are structurally equivalent.
-
If a and b are non-record object references, and they refer to the same object, then a and b are structurally equivalent. So structural equivalence agrees with
identical()
for non-records. -
Otherwise a and b are not structurally equivalent.
With that definition, the rules for object and collection canonicalization is
changed from requiring that instance variable, list/set element and map
key/value values are identical()
between the instances, to them being
structurally equivalent.
This change allows a class like:
class C {
final (int, int) pair;
const C(int x, int y) : pair = (x, y);
}
to be properly canonicalized for objects with the same effective state,
independently of whether identical()
returns true
or false
on the pair
value. Notice that if the identical()
returns true
on two records, they must
be structurally equivalent, but unlike for non-records, the identical()
function can also return false
for structurally equivalent records.
It is a compile-time error if a record type or a type alias that resolves to a record type is used in:
- An
extends
clause. - An
implements
clause. - A
with
clause. - An
on
clause on a mixin declaration. A record type can be used as theon
type in an extension declaration.
The fields in a record expression are evaluated left to right. This is true even if an implementation chooses to reorder the named fields in order to canonicalize records with the same set of named fields. For example:
int say(int i) {
print(i);
return i;
}
var x = (a: say(1), b: say(2));
var y = (b: say(3), a: say(4));
This program must print "1", "2", "3", "4", even though x
and y
are
records with the same shape.
Each field in the record's shape exposes a corresponding getter. Invoking that getter returns the value provided for that field when the record was created. Record fields are immutable and do not have setters.
In debug builds, the toString()
method converts each field to a string by
calling toString()
on its value and prepending it with the field name followed
by :
if the field is named. It concatenates these with ,
as a separator
and returns the resulted surrounded by parentheses. For example:
print((1, 2, 3).toString()); // "(1, 2, 3)".
print((a: 'str', 'int').toString()); // "(a: str, int)".
The order that named fields appear and how they are interleaved with positional fields is unspecified. Positional fields must appear in position order. This gives implementations freedom to choose a canonical order for named fields independent of the order that the record was created with.
In a release or optimized build, the behavior of toString()
is unspecified.
This gives implementations freedom to discard the full names of named fields in
order to reduce code size. Users should only use toString()
on records for
debugging purposes. They are strongly discouraged from parsing the results of
calling toString()
or relying on it for end-user visible output.
Records have value equality, which means two records are equal if they have the same shape and the corresponding fields are equal. Since named field order is not part of a record's shape, that implies that the order of named fields does not affect equality:
var a = (x: 1, 2);
var b = (2, x: 1);
print(a == b); // true.
More precisely, the ==
method on record r
with right operand o
is defined
as:
-
If
o
is not a record with the same shape asr
thenfalse
. -
For each pair of corresponding fields
rf
andof
in unspecified order:- If
rf == of
isfalse
thenfalse
.
- If
-
Else,
true
.
The order that fields are iterated is potentially user-visible since
user-defined ==
methods can have side effects. Most well-behaved ==
implementations are pure. The order that fields are visited is deliberately left
unspecified so that implementations are free to reorder the field comparisons
for performance.
The implementation of hashCode
follows this. The hash code returned should
depend on the field values such that two records that compare equal must have
the same hash code.
A record object has a primitive ==
operator if all of its field have primitive
==
operators.
Note that this is a dynamic property of a record object, not a property of its static type. Since primitive equality only comes into play in constants, the compiler can see the actual field values for a relevant record at compile time because it has the actual constant record value with all of its constant fields. This means a record can be used in a constant set or as a constant map key, but only when its field values could be as well.
We expect records to often be used for multiple return values. In that case, and
in others, we would like compilers to be able to easily optimize away the heap
allocation and initialization of the record object. If we require each record
to have a persistent identity that is tied to its creation and user visible
through calls to identical()
, then optimizing away the creation of these
objects is harder.
Semantically, we do not want records to have unique identities distinct from
their contents. A record is its contents in the same way that every value 3 in
a program is the "same" 3 whether it came from the number literal 3
or the
result of 1 + 2
. This is why ==
for records is defined in terms of their
shape and fields. Two records with the same shape and equal fields are equal
values.
At the same time, we want identical()
to be fast because one of its primary
uses is as a fast-path check for equality. An identical()
that is obliged to
iterate over the record's fields (transitively in the case where some fields
are themselves records) might nullify the benefits of using identical()
as a
fast-path check before calling ==
.
To balance those opposing goals, identical()
on records is defined to only
offer loose guarantees. Calling identical()
with a record argument returns:
false
, if the other argument is not a record.false
, if the records do not have the same shape. Since named field order is not part of a record's shape, this implies that named field order does not affect identity either.identical((a: 1, b: 2), (b: 2, a: 1))
is not required to return false.false
, if any pair of corresponding fields are not identical.- Otherwise it may return
true
, but is not required to.
If an implementation can easily determine that two record arguments to
identical()
have the same shape and identical fields, then it should return
true
. Typically, this is because the two arguments to identical()
are
pointers with the same address to the same heap-allocated record object. But if
an implementation would have to do a slower field-wise comparison to determine
identity, it's probably better to return false
quickly.
In other words, if identical()
returns true
, then the records are
definitely indistinguishable. But if it returns false
, they may or may not
be.
Like numbers, records do not have a well-defined persistent identity. That means Expandos can not be attached to them.
The runtime type of a record is determined from the runtime types of its fields. There is no notion of a separate, explicitly reified type. So, here:
(num, Object) pair = (1, 2.3);
print(pair is (int, double)); // "true".
The runtime type of pair
is (int, double)
, not (num, Object)
, However, the
variable declaration is still valid and sound because records are naturally
covariant in their field types.
The records feature is language versioned, as usual with new Dart features. This
means that it will be an error to use the syntax for records in libraries which
do not have a language version greater than or equal to the language version in
which records are released. More specifically, assuming that v
is the language
version in which records are released, the following errors apply.
It is an error for the record literal syntax (e.g. (3, 4)
) to be used
syntactically in a library whose language version is less than v
.
It is an error for the record type syntax (e.g. (int, int)
) to be used
syntactically in a library whose language version is less than v
.
Note that the above errors only apply to direct syntactic uses of the new
record syntax in legacy libraries. It is not an error for a library whose
language version is less than v
(a "legacy library") to include types which
denote or include the Record
class, record types or record expressions when
these terms arise directly or indirectly from references to another library
whose language version is greater than or equal to v
.
For example, such a legacy library may reference a typedef name which is bound to a record type in another library, and the semantic interpretation of the typedef is as the underlying record type, just as it would be for any other type. Similarly, type inference may introduce record types into a legacy library, and such types will be interpreted by the compiler as record types as usual (that is, there is no erasure implied to remove these inferred types).
Record values may flow into a legacy library via a reference to a member from another library, and a legacy library may freely call getters on record values (since there is no new syntax for calling a record getter). The rationale for the choices described in this section is that the intent of language versioning (for an additive feature such as records) is to ensure that users do not accidentally use new features in a package without specifying an SDK constraint which ensures that their code will always be run on an SDK which supports the feature. But in the case of a legacy library which references record values or types indirectly via another library, the SDK constraint on the referenced library is sufficient to enforce this.
- Specify handling of record types with
NNBD_TOP_MERGE
.
- Start positional record field getters at
$1
, not$0
(#2638).
- Allow legacy libraries that don't support records to still be able to see the
Record
class in "dart:core" (#2661).
- Unconditionally treat
on
after atry
block as an on clause even when not ambiguous (#2599).
- Disambiguate record types following metadata annotations (#2469).
- Consistently disallow private and Object member names as positional and named field names in record expressions and types (#2575).
- Specify the interaction between libraries with a language version that supports records and libraries with older language versions.
- Specify type inference, add static semantics to resources/type-system.
- Introduce
()
syntax for empty record expressions and removeRecord.empty
.
-
Include record types in
typeNotVoid
. This allows them to appear inis
andas
expressions (which was always intended). -
Clarify that record types cannot be used as supertypes or superinterfaces.
- Revert back to disallowing private field names in records.
-
Allow private named fields in records (#2387).
-
Allow positional fields in record types named
hashCode
,runtimeType
,noSuchMethod
, ortoString
.
- Specify that a record has a primitive
==
when its fields all do.
- Move to
accepted/
.
- Clarify what kind of type
Record
is and where it's defined (#2442).
-
Support constant records (#2337).
-
Support empty and one-positional-field records (#2386).
-
Re-add support for positional field getters (#2388).
-
Specify the behavior of
toString()
(#2389). -
Disambiguate record types in
on
clauses (#2406). -
Clarify the order that fields are evaluated in record expressions.
-
Clarify the iteration order of fields in
==
.
-
Make the grammar for record types closer to function type parameter lists. Allow metadata before fields and optional names for positional fields.
-
Weave
recordType
into the grammar better. Don't allow it in inheritance clauses, but do allow it as the return type of function types. -
Remove shorthand syntax that elides parentheses when there are no positional fields since that's ambiguous inside a function type (#2302).
-
Clarify that there is no record type literal syntax (#2304).
-
Remove the reflective static members on
Record
. Like other reflective features, supporting these operations may incur a global cost in generated code size for unknown benefit (#1275, #1277). -
Remove support for single positional element records. They don't have any current use and are a syntactic wart. If we later add support for spreading argument lists and single element positional records become useful, we can re-add them then.
-
Remove synthesized getters for positional fields. This avoids problems if a positional field's synthesized getter collides with an explicit named field (#1291).
- Remove the
Destructure_n_
interfaces.
- Remove the static methods on
Record
(#2127).
- Minor copy editing and clean up.