proposals/csharp-10.0/record-structs.md
[!INCLUDESpecletdisclaimer]
Champion issue: https://github.com/dotnet/csharplang/issues/4334
The syntax for a record struct is as follows:
record_struct_declaration
: attributes? struct_modifier* 'partial'? 'record' 'struct' identifier type_parameter_list?
parameter_list? struct_interfaces? type_parameter_constraints_clause* record_struct_body
;
record_struct_body
: struct_body
| ';'
;
Record struct types are value types, like other struct types. They implicitly inherit from the class System.ValueType.
The modifiers and members of a record struct are subject to the same restrictions as those of structs
(accessibility on type, modifiers on members, base(...) instance constructor initializers,
definite assignment for this in constructor, destructors, ...).
Record structs will also follow the same rules as structs for parameterless instance constructors and field initializers,
but this document assumes that we will lift those restrictions for structs generally.
See §16.4.9 See parameterless struct constructors spec.
Record structs cannot use ref modifier.
At most one partial type declaration of a partial record struct may provide a parameter_list.
The parameter_list may be empty.
Record struct parameters cannot use ref, out or this modifiers (but in and params are allowed).
In addition to the members declared in the record struct body, a record struct type has additional synthesized members. Members are synthesized unless a member with a "matching" signature is declared in the record struct 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. See Signatures and overloading §7.6. It is an error for a member of a record struct to be named "Clone".
It is an error for an instance field of a record struct to have an unsafe type.
A record struct is not permitted to declare a destructor.
The synthesized members are as follows:
The synthesized equality members are similar as in a record class (Equals for this type, Equals for object type, == and != operators for this type),
except for the lack of EqualityContract, null checks or inheritance.
The record struct implements System.IEquatable<R> and includes a synthesized strongly-typed overload of Equals(R other) where R is the record struct.
The method is public.
The method can be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility.
If Equals(R other) is user-defined (not synthesized) but GetHashCode is not, a warning is produced.
public readonly bool Equals(R other);
The synthesized Equals(R) returns true if and only if for each instance field fieldN in the record struct
the value of System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN) where TN is the field type is true.
The record struct includes synthesized == and != operators equivalent to operators declared as follows:
public static bool operator==(R r1, R r2)
=> r1.Equals(r2);
public static bool operator!=(R r1, R r2)
=> !(r1 == r2);
The Equals method called by the == operator is the Equals(R other) method specified above. The != operator delegates to the == operator. It is an error if the operators are declared explicitly.
The record struct includes a synthesized override equivalent to a method declared as follows:
public override readonly bool Equals(object? obj);
It is an error if the override is declared explicitly.
The synthesized override returns other is R temp && Equals(temp) where R is the record struct.
The record struct includes a synthesized override equivalent to a method declared as follows:
public override readonly int GetHashCode();
The method can be declared explicitly.
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 combining the values of System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN) for each instance field fieldN with TN being the type of fieldN.
For example, consider the following record struct:
record struct R1(T1 P1, T2 P2);
For this record struct, the synthesized equality members would be something like:
struct R1 : IEquatable<R1>
{
public T1 P1 { get; set; }
public T2 P2 { get; set; }
public override bool Equals(object? obj) => obj is R1 temp && Equals(temp);
public bool Equals(R1 other)
{
return
EqualityComparer<T1>.Default.Equals(P1, other.P1) &&
EqualityComparer<T2>.Default.Equals(P2, other.P2);
}
public static bool operator==(R1 r1, R1 r2)
=> r1.Equals(r2);
public static bool operator!=(R1 r1, R1 r2)
=> !(r1 == r2);
public override int GetHashCode()
{
return Combine(
EqualityComparer<T1>.Default.GetHashCode(P1),
EqualityComparer<T2>.Default.GetHashCode(P2));
}
}
The record struct includes a synthesized method equivalent to a method declared as follows:
private bool PrintMembers(System.Text.StringBuilder builder);
The method does the following:
For a member that has a value type, we will convert its value to a string representation using the most efficient method available to the target platform. At present that means calling ToString before passing to StringBuilder.Append.
If the record's printable members do not include a readable property with a non-readonly get accessor, then the synthesized PrintMembers is readonly. There is no requirement for the record's fields to be readonly for the PrintMembers method to be readonly.
The PrintMembers method can be declared explicitly.
It is an error if the explicit declaration does not match the expected signature or accessibility.
The record struct includes a synthesized method equivalent to a method declared as follows:
public override string ToString();
If the record struct's PrintMembers method is readonly, then the synthesized ToString() method is readonly.
The method can be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility.
The synthesized method:
StringBuilder instance,PrintMembers method giving it the builder, followed by " " if it returned true,builder.ToString().For example, consider the following record struct:
record struct R1(T1 P1, T2 P2);
For this record struct, the synthesized printing members would be something like:
struct R1 : IEquatable<R1>
{
public T1 P1 { get; set; }
public T2 P2 { get; set; }
private bool PrintMembers(StringBuilder builder)
{
builder.Append(nameof(P1));
builder.Append(" = ");
builder.Append(this.P1); // or builder.Append(this.P1.ToString()); if P1 has a value type
builder.Append(", ");
builder.Append(nameof(P2));
builder.Append(" = ");
builder.Append(this.P2); // or builder.Append(this.P2.ToString()); if P2 has a value type
return true;
}
public override string ToString()
{
var builder = new StringBuilder();
builder.Append(nameof(R1));
builder.Append(" { ");
if (PrintMembers(builder))
builder.Append(" ");
builder.Append("}");
return builder.ToString();
}
}
In addition to the above members, record structs with a parameter list ("positional records") synthesize additional members with the same conditions as the members above.
A record struct has a public constructor whose signature corresponds to the value parameters of the type declaration. This is called the primary constructor for the type. It is an error to have a primary constructor and a constructor with the same signature already present in the struct. If the type declaration does not include a parameter list, no primary constructor is generated.
record struct R1
{
public R1() { } // ok
}
record struct R2()
{
public R2() { } // error: 'R2' already defines constructor with same parameter types
}
Instance field declarations for a record struct are permitted to include variable initializers. If there is no primary constructor, the instance initializers execute as part of the parameterless constructor. Otherwise, at runtime the primary constructor executes the instance initializers appearing in the record-struct-body.
If a record struct has a primary constructor, any user-defined constructor must have an
explicit this constructor initializer that calls the primary constructor or an explicitly declared constructor.
Parameters of the primary constructor as well as members of the record struct are in scope 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.
A warning is produced if a parameter of the primary constructor is not read.
The definite assignment rules for struct instance constructors apply to the primary constructor of record structs. For instance, the following is an error:
record struct Pos(int X) // definite assignment error in primary constructor
{
private int x;
public int X { get { return x; } set { x = value; } } = X;
}
For each record struct parameter of a record struct declaration there is a corresponding public property member whose name and type are taken from the value parameter declaration.
For a record struct:
get and init auto-property is created if the record struct has readonly modifier, get and set otherwise.
Both kinds of set accessors (set and init) are considered "matching". So the user may declare an init-only property
in place of a synthesized mutable one.
An inherited abstract property with matching type is overridden.
No auto-property is created if the record struct has an instance field with expected name and type.
It is an error if the inherited property does not have public get and set/init accessors.
It is an error if the inherited property or field is hidden.property: or field:
targets for attributes syntactically applied to the corresponding record struct parameter.A positional record struct with at least one parameter synthesizes a public void-returning instance 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.
If the instance members accessed in the body do not include a property with
a non-readonly get accessor, then the synthesized Deconstruct method is readonly.
The method can be declared explicitly. It is an error if the explicit declaration does not match
the expected signature or accessibility, or is static.
with expression on structsIt is now valid for the receiver in a with expression to have a struct type.
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 receiver's
type.
For a receiver with struct type, the receiver is first copied, then each member_initializer is processed
the same way as an assignment to a field or property access of the result of the conversion.
Assignments are processed in lexical order.
record classThe existing syntax for record types allows record class with the same meaning as record:
record_declaration
: attributes? class_modifier* 'partial'? 'record' 'class'? identifier type_parameter_list?
parameter_list? record_base? type_parameter_constraints_clause* record_body
;
No auto-property is created if the record has or inherits an instance field with expected name and type.
See parameterless struct constructors spec.
bool) (answer: yes)record ref struct (issue with IEquatable<RefStruct> and ref fields) (answer: yes)bool Equals(R other), bool Equals(object? other) and operators all just delegate to ValueType.Equals. (answer: yes)Combine method? (answer: as little as possible)Equals logic is functionally equivalent to runtime implementation (e.g. float.NaN) (answer: confirmed in LDM)with on generics? (answer: out of scope for C# 10)GetHashCode include a hash of the type itself, to get different values between record struct S1; and record struct S2;? (answer: no)