12 KiB
Records
This proposal tracks the specification for the C# 9 records feature, as agreed to by the C# language design team.
The syntax for a record is as follows:
record_declaration
: attributes? class_modifier* 'partial'? 'record' identifier type_parameter_list?
parameter_list? record_base? type_parameter_constraints_clause* record_body
;
record_base
: ':' class_type argument_list?
| ':' interface_type_list
| ':' class_type argument_list? interface_type_list
;
record_body
: '{' class_member_declaration* '}'
| ';'
;
Record types are reference types, similar to a class declaration. It is an error for a record to provide
a record_base
argument_list
if the record_declaration
does not contain a parameter_list
.
Members of a record type
In addition to the members declared in the record body, a record type has additional synthesized members. Members are synthesized unless a member with a "matching" signature is declared in the record body or an accessible concrete non-virtual member with a "matching" signature is inherited. Two members are considered matching if they have the same signature or would be considered "hiding" in an inheritance scenario.
The synthesized members are as follows:
Equality members
The record type includes a synthesized EqualityContract
readonly virtual property. The property is overridden in each derived record type.
The property can be declared explicitly.
It is an error if the explicit declaration does not match the expected signature or accessibility, or if the explicit declaration is not virtual
and the record type is not sealed
.
The synthesized property returns typeof(R)
where R
is the record type.
protected virtual Type EqualityContract { get; };
Can we omit EqualityContract
if the record type is sealed
and derives from System.Object
?
The record type implements System.IEquatable<R>
and includes a synthesized strongly-typed overload of Equals(R? other)
where R
is the record type.
The method is public
, and the method is virtual
unless the record type is sealed
.
The method can be declared explicitly.
It is an error if the explicit declaration does not match the expected signature or accessibility, or the explicit declaration is not virtual
and the record type is not sealed
.
public virtual bool Equals(R? other);
The synthesized Equals(R?)
returns true
if and only if each of the following are true
:
other
is notnull
, and- For each instance field
fieldN
in the record type that is not inherited, the value ofSystem.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN)
whereTN
is the field type, and - If there is a base record type, the value of
base.Equals(other)
(a non-virtual call topublic virtual bool Equals(Base? other)
); otherwise the value ofEqualityContract == other.EqualityContract
.
If the record type is derived from a base record type Base
, the record type includes a synthesized override of the strongly-typed Equals(Base other)
.
The synthesized override is sealed
.
It is an error if the override is declared explicitly.
The synthesized override returns Equals((object?)other)
.
The record type includes a synthesized override of object.Equals(object? obj)
.
It is an error if the override is declared explicitly.
The synthesized override returns Equals(other as R)
where R
is the record type.
public override bool Equals(object? obj);
The record type includes a synthesized override of object.GetHashCode()
.
The method can be declared explicitly.
It is an error if the explicit declaration is sealed
unless the record type is sealed
.
public override int GetHashCode();
A warning is reported if one of Equals(R?)
and GetHashCode()
is explicitly declared but the other method is not explicit.
The synthesized override of GetHashCode()
returns an int
result of a deterministic function combining the following values:
- For each instance field
fieldN
in the record type that is not inherited, the value ofSystem.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN)
whereTN
is the field type, and - If there is a base record type, the value of
base.GetHashCode()
; otherwise the value ofSystem.Collections.Generic.EqualityComparer<System.Type>.Default.GetHashCode(EqualityContract)
.
For example, consider the following record types:
record R1(T1 P1);
record R2(T1 P1, T2 P2) : R1(P1);
record R2(T1 P1, T2 P2, T3 P3) : R2(P1, P2);
For those record types, the synthesized members would be something like:
class R1 : IEquatable<R1>
{
public T1 P1 { get; set; }
protected virtual Type EqualityContract => typeof(R1);
public override bool Equals(object? obj) => Equals(obj as R1);
public virtual bool Equals(R1? other)
{
return !(other is null) &&
EqualityContract == other.EqualityContract &&
EqualityComparer<T1>.Default.Equals(P1, other.P1);
}
public override int GetHashCode()
{
return Combine(EqualityComparer<Type>.Default.GetHashCode(EqualityContract),
EqualityComparer<T1>.Default.GetHashCode(P1));
}
}
class R2 : R1, IEquatable<R2>
{
public T2 P2 { get; set; }
protected override Type EqualityContract => typeof(R2);
public override bool Equals(object? obj) => Equals(obj as R2);
public sealed override bool Equals(R1? other) => Equals((object?)other);
public virtual bool Equals(R2? other)
{
return base.Equals((R1?)other) &&
EqualityComparer<T2>.Default.Equals(P2, other.P2);
}
public override int GetHashCode()
{
return Combine(base.GetHashCode(),
EqualityComparer<T2>.Default.GetHashCode(P2));
}
}
class R3 : R2, IEquatable<R3>
{
public T3 P3 { get; set; }
protected override Type EqualityContract => typeof(R3);
public override bool Equals(object? obj) => Equals(obj as R3);
public sealed override bool Equals(R2? other) => Equals((object?)other);
public virtual bool Equals(R3? other)
{
return base.Equals((R2?)other) &&
EqualityComparer<T3>.Default.Equals(P3, other.P3);
}
public override int GetHashCode()
{
return Combine(base.GetHashCode(),
EqualityComparer<T3>.Default.GetHashCode(P3));
}
}
Copy and Clone members
A record type contains two copying members:
- A constructor taking a single argument of the record type. It is referred to as a "copy constructor".
- A synthesized public parameterless virtual instance "clone" method with a compiler-reserved name
The purpose of the copy constructor is to copy the state from the parameter to the new instance being created. This constructor doesn't run any instance field/property initializers present in the record declaration. If the constructor is not explicitly declared, a protected constructor will be synthesized by the compiler. The first thing the constructor must do, is to call a copy constructor of the base, or a parameter-less object constructor if the record inherits from object. An error is reported if a user-defined copy constructor uses an implicit or explicit constructor initializer that doesn't fulfill this requirement. After a base copy constructor is invoked, a synthesized copy constructor copies values for all instance fields implicitly or explicitly declared within the record type.
The "clone" method returns the result of a call to a constructor with the same signature as the copy constructor. The return type of the clone method is the containing type, unless a virtual clone method is present in the base class. In that case, the return type is the current containing type if the "covariant returns" feature is supported and the override return type otherwise. The synthesized clone method is an override of the base type clone method if one exists. An error is produced if the base type clone method is sealed.
If the containing record is abstract, the synthesized clone method is also abstract.
Positional record members
In addition to the above members, records with a parameter list ("positional records") synthesize additional members with the same conditions as the members above.
Primary Constructor
A record type has a public constructor whose signature corresponds to the value parameters of the type declaration. This is called the primary constructor for the type, and causes the implicitly declared default class constructor, if present, to be suppressed. It is an error to have a primary constructor and a constructor with the same signature already present in the class.
At runtime the primary constructor
-
executes the instance initializers appearing in the class-body
-
invokes the base class constructor with the arguments provided in the
record_base
clause, if present
If a record has a primary constructor, any user-defined constructor, except "copy constructor" must have an
explicit this
constructor initializer.
Parameters of the primary constructor as well as members of the record are in scope within the argument_list
of the record_base
clause and within initializers of instance fields or properties. Instance members would
be an error in these locations (similar to how instance members are in scope in regular constructor initializers
today, but an error to use), but the parameters of the primary constructor would be in scope and useable and
would shadow members. Static members would also be useable, similar to how base calls and initializers work in
ordinary constructors today.
Expression variables declared in the argument_list
are in scope within the argument_list
. The same shadowing
rules as within an argument list of a regular constructor initializer apply.
Properties
For each record parameter of a record type declaration there is a corresponding public property member whose name and type are taken from the value parameter declaration.
For a record:
- A public
get
andinit
auto-property is created (see separateinit
accessor specification). Each "matching" inherited abstract accessor is overridden. The auto-property is initialized to the value of the corresponding primary constructor parameter.
Deconstruct
A positional record synthesizes a public void-returning method called Deconstruct with an out parameter declaration for each parameter of the primary constructor declaration. Each parameter of the Deconstruct method has the same type as the corresponding parameter of the primary constructor declaration. The body of the method assigns each parameter of the Deconstruct method to the value from an instance member access to a member of the same name.
with
expression
A with
expression is a new expression using the following syntax.
with_expression
: switch_expression
| switch_expression 'with' '{' member_initializer_list? '}'
;
member_initializer_list
: member_initializer (',' member_initializer)*
;
member_initializer
: identifier '=' expression
;
A with
expression allows for "non-destructive mutation", designed to
produce a copy of the receiver expression with modifications in assignments
in the member_initializer_list
.
A valid with
expression has a receiver with a non-void type. The receiver type must contain an
accessible synthesized record "clone" method.
On the right hand side of the with
expression is a member_initializer_list
with a sequence
of assignments to identifier, which must be an accessible instance field or property of the return
type of the Clone()
method.
Each member_initializer
is processed the same way as an assignment to a field or property
access of the return value of the record clone method. The clone method is executed only once
and the assignments are processed in lexical order.