Movatterモバイル変換


[0]ホーム

URL:


Skip to main content

This browser is no longer supported.

Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.

Download Microsoft EdgeMore info about Internet Explorer and Microsoft Edge
Table of contentsExit focus mode

Records

  • 2023-02-08
Feedback

In this article

Note

This article is a feature specification. The specification serves as the design document for the feature. It includes proposed specification changes, along with information needed during the design and development of the feature. These articles are published until the proposed spec changes are finalized and incorporated in the current ECMA specification.

There may be some discrepancies between the feature specification and the completed implementation. Those differences are captured in the pertinentlanguage design meeting (LDM) notes.

You can learn more about the process for adopting feature speclets into the C# language standard in the article on thespecifications.

Champion issue:https://github.com/dotnet/csharplang/issues/39

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 providearecord_baseargument_list if therecord_declaration does not contain aparameter_list.At most one partial type declaration of a partial record may provide aparameter_list.

Record parameters cannot useref,out orthis modifiers (butin andparams are allowed).

Inheritance

Records cannot inherit from classes, unless the class isobject, and classes cannot inherit from records. Records can inherit from other records.

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 oran accessible concrete non-virtual member with a "matching" signature is inherited. A matching member prevents the compiler from generating that member, not any other synthesized members.Two members are considered matching if they have the samesignature or would be considered "hiding" in an inheritance scenario.It is an error for a member of a record to be named "Clone".It is an error for an instance field of a record to have a top-level pointer type. A nested pointer type, such as an array of pointers, is allowed.

The synthesized members are as follows:

Equality members

If the record is derived fromobject, the record type includes a synthesized readonly property equivalent to a property declared as follows:

Type EqualityContract { get; }

The property isprivate if the record type issealed. Otherwise, the property isvirtual andprotected.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 doesn't allow overriding it in a derived type and the record type is notsealed.

If the record type is derived from a base record typeBase, the record type includes a synthesized readonly property equivalent to a property declared as follows:

protected override Type EqualityContract { get; }

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 doesn't allow overriding it in a derived type and the record type is notsealed. It is an error if either synthesized, or explicitly declared property doesn't override a property with this signature in the record typeBase (for example, if the property is missing in theBase, or sealed, or not virtual, etc.).The synthesized property returnstypeof(R) whereR is the record type.

The record type implementsSystem.IEquatable<R> and includes a synthesized strongly-typed overload ofEquals(R? other) whereR is the record type.The method ispublic, and the method isvirtual unless the record type issealed.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 doesn't allow overriding it in a derived type and the record type is notsealed.

IfEquals(R? other) is user-defined (not synthesized) butGetHashCode is not, a warning is produced.

public virtual bool Equals(R? other);

The synthesizedEquals(R?) returnstrue if and only if each of the following aretrue:

  • other is notnull, and
  • For each instance fieldfieldN 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 ofbase.Equals(other) (a non-virtual call topublic virtual bool Equals(Base? other)); otherwisethe value ofEqualityContract == other.EqualityContract.

The record type includes synthesized== and!= operators equivalent to operators declared as follows:

public static bool operator==(R? left, R? right)    => (object)left == right || (left?.Equals(right) ?? false);public static bool operator!=(R? left, R? right)    => !(left == right);

TheEquals method called by the== operator is theEquals(R? other) method specified above. The!= operator delegates to the== operator. It is an error if the operators are declared explicitly.

If the record type is derived from a base record typeBase, the record type includes a synthesized override equivalent to a method declared as follows:

public sealed override bool Equals(Base? other);

It is an error if the override is declared explicitly. It is an error if the method doesn't override a method with same signature in record typeBase (for example, if the method is missing in theBase, or sealed, or not virtual, etc.).The synthesized override returnsEquals((object?)other).

The record type includes a synthesized override equivalent to a method declared as follows:

public override bool Equals(object? obj);

It is an error if the override is declared explicitly. It is an error if the method doesn't overrideobject.Equals(object? obj) (for example, due to shadowing in intermediate base types, etc.).The synthesized override returnsEquals(other as R) whereR is the record type.

The record type includes a synthesized override equivalent to a method declared as follows:

public override int GetHashCode();

The method can be declared explicitly.It is an error if the explicit declaration doesn't allow overriding it in a derived type and the record type is notsealed. It is an error if either synthesized, or explicitly declared method doesn't overrideobject.GetHashCode() (for example, due to shadowing in intermediate base types, etc.).

A warning is reported if one ofEquals(R?) andGetHashCode() is explicitly declared but the other method is not explicit.

The synthesized override ofGetHashCode() returns anint result of combining the following values:

  • For each instance fieldfieldN 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 ofbase.GetHashCode(); otherwisethe 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 R3(T1 P1, T2 P2, T3 P3) : R2(P1, P2);

For those record types, the synthesized equality members would be something like:

class R1 : IEquatable<R1>{    public T1 P1 { get; init; }    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 static bool operator==(R1? left, R1? right)        => (object)left == right || (left?.Equals(right) ?? false);    public static bool operator!=(R1? left, R1? right)        => !(left == right);    public override int GetHashCode()    {        return HashCode.Combine(EqualityComparer<Type>.Default.GetHashCode(EqualityContract),            EqualityComparer<T1>.Default.GetHashCode(P1));    }}class R2 : R1, IEquatable<R2>{    public T2 P2 { get; init; }    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 static bool operator==(R2? left, R2? right)        => (object)left == right || (left?.Equals(right) ?? false);    public static bool operator!=(R2? left, R2? right)        => !(left == right);    public override int GetHashCode()    {        return HashCode.Combine(base.GetHashCode(),            EqualityComparer<T2>.Default.GetHashCode(P2));    }}class R3 : R2, IEquatable<R3>{    public T3 P3 { get; init; }    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 static bool operator==(R3? left, R3? right)        => (object)left == right || (left?.Equals(right) ?? false);    public static bool operator!=(R3? left, R3? right)        => !(left == right);    public override int GetHashCode()    {        return HashCode.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 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 beingcreated. This constructor doesn't run any instance field/property initializers present in the recorddeclaration. If the constructor is not explicitly declared, a constructor will be synthesizedby the compiler. If the record is sealed, the constructor will be private, otherwise it will be protected.An explicitly declared copy constructor must be either public or protected, unless therecord is sealed.The first thing the constructor must do, is to call a copy constructor of the base, or a parameter-lessobject constructor if the record inherits from object. An error is reported if a user-defined copyconstructor 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 instancefields implicitly or explicitly declared within the record type.The sole presence of a copy constructor, whether explicit or implicit, doesn't prevent an automaticaddition of a default instance constructor.

If a virtual "clone" method is present in the base record, the synthesized "clone" method overrides it andthe return type of the method is the current containing type. An error is produced if the base record clone method is sealed.If a virtual "clone" method is not present in the base record, the return type of the clone methodis the containing type and the method is virtual, unless the record is sealed or abstract.If the containing record is abstract, the synthesized clone method is also abstract.If the "clone" method is not abstract, it returns the result of a call to a copy constructor.

Printing members: PrintMembers and ToString methods

If the record is derived fromobject, the record includes a synthesized method equivalent to a method declared as follows:

bool PrintMembers(System.Text.StringBuilder builder);

The method isprivate if the record type issealed. Otherwise, the method isvirtual andprotected.

The method:

  1. calls the methodSystem.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack() if the method is present and the record has printable members.
  2. for each of the record's printable members (non-static public field and readable property members), appends that member's name followed by " = " followed by the member's value separated with ", ",
  3. return true if the record has printable members.

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 callingToString before passing toStringBuilder.Append.

If the record type is derived from a base recordBase, the record includes a synthesized override equivalent to a method declared as follows:

protected override bool PrintMembers(StringBuilder builder);

If the record has no printable members, the method calls the basePrintMembers method with one argument (itsbuilder parameter) and returns the result.

Otherwise, the method:

  1. calls the basePrintMembers method with one argument (itsbuilder parameter),
  2. if thePrintMembers method returned true, append ", " to the builder,
  3. for each of the record's printable members, appends that member's name followed by " = " followed by the member's value:this.member (orthis.member.ToString() for value types), separated with ", ",
  4. return true.

ThePrintMembers method 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 doesn't allow overriding it in a derived type and the record type is notsealed.

The record includes a synthesized method equivalent to a method declared as follows:

public override string ToString();

The method 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 doesn't allow overriding it in a derived type and the record type is notsealed. It is an error if either synthesized, or explicitly declared method doesn't overrideobject.ToString() (for example, due to shadowing in intermediate base types, etc.).

The synthesized method:

  1. creates aStringBuilder instance,
  2. appends the record name to the builder, followed by " { ",
  3. invokes the record'sPrintMembers method giving it the builder, followed by " " if it returned true,
  4. appends "}",
  5. returns the builder's contents withbuilder.ToString().

For example, consider the following record types:

record R1(T1 P1);record R2(T1 P1, T2 P2, T3 P3) : R1(P1);

For those record types, the synthesized printing members would be something like:

class R1 : IEquatable<R1>{    public T1 P1 { get; init; }        protected virtual bool PrintMembers(StringBuilder builder)    {        builder.Append(nameof(P1));        builder.Append(" = ");        builder.Append(this.P1); // or builder.Append(this.P1.ToString()); if T1 is 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();    }}class R2 : R1, IEquatable<R2>{    public T2 P2 { get; init; }    public T3 P3 { get; init; }        protected override bool PrintMembers(StringBuilder builder)    {        if (base.PrintMembers(builder))            builder.Append(", ");                    builder.Append(nameof(P2));        builder.Append(" = ");        builder.Append(this.P2); // or builder.Append(this.P2); if T2 is a value type                builder.Append(", ");                builder.Append(nameof(P3));        builder.Append(" = ");        builder.Append(this.P3); // or builder.Append(this.P3); if T3 is a value type                return true;    }        public override string ToString()    {        var builder = new StringBuilder();        builder.Append(nameof(R2));        builder.Append(" { ");        if (PrintMembers(builder))            builder.Append(" ");        builder.Append("}");        return builder.ToString();    }}

Positional record members

In addition to the above members, records with a parameter list ("positional records") synthesizeadditional 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 thetype declaration. This is called the primary constructor for the type, and causes the implicitlydeclared default class constructor, if present, to be suppressed. It is an error to have a primaryconstructor and a constructor with the same signature already present in the class.

At runtime the primary constructor

  1. executes the instance initializers appearing in the class-body

  2. invokes the base class constructor with the arguments provided in therecord_base clause, if present

If a record has a primary constructor, any user-defined constructor, except "copy constructor" must have anexplicitthis constructor initializer.

Parameters of the primary constructor as well as members of the record are in scope within theargument_listof therecord_base clause and within initializers of instance fields or properties. Instance members wouldbe an error in these locations (similar to how instance members are in scope in regular constructor initializerstoday, but an error to use), but the parameters of the primary constructor would be in scope and useable andwould shadow members. Static members would also be useable, similar to how base calls and initializers work inordinary constructors today.

A warning is produced if a parameter of the primary constructor is not read.

Expression variables declared in theargument_list are in scope within theargument_list. The same shadowingrules 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 propertymember whose name and type are taken from the value parameter declaration.

For a record:

  • A publicget andinit auto-property is created (see separateinit accessor specification).An inheritedabstract property with matching type is overridden.It is an error if the inherited property does not havepublic overridableget andinit accessors.It is an error if the inherited property is hidden.
    The auto-property is initialized to the value of the corresponding primary constructor parameter.Attributes can be applied to the synthesized auto-property and its backing field by usingproperty: orfield:targets for attributes syntactically applied to the corresponding record parameter.

Deconstruct

A positional record with at least one parameter synthesizes a public void-returning instance method called Deconstruct with an outparameter declaration for each parameter of the primary constructor declaration. Each parameterof theDeconstruct method has the same type as the corresponding parameter of the primaryconstructor declaration. The body of the method assigns to each parameter of theDeconstruct method,the value of the instance property of the same name.The method can be declared explicitly. It is an error if the explicit declaration does not matchthe expected signature or accessibility, or is static.

The following example shows a positional recordR with its compiler synthesizedDeconstruct method, along with its usage:

public record R(int P1, string P2 = "xyz"){    public void Deconstruct(out int P1, out string P2)    {        P1 = this.P1;        P2 = this.P2;    }}class Program{    static void Main()    {        R r = new R(12);        (int p1, string p2) = r;        Console.WriteLine($"p1: {p1}, p2: {p2}");    }}

with expression

Awith 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    ;

Awith expression is not permitted as a statement.

Awith expression allows for "non-destructive mutation", designed toproduce a copy of the receiver expression with modifications in assignmentsin themember_initializer_list.

A validwith expression has a receiver with a non-void type. The receiver type must be a record.

On the right hand side of thewith expression is amember_initializer_list with a sequenceof assignments toidentifier, which must be an accessible instance field or property of the receiver'stype.

First, receiver's "clone" method (specified above) is invoked and its result is converted to thereceiver's type. Then, eachmember_initializer is processed the same way as an assignment toa field or property access of the result of the conversion. Assignments are processed in lexical order.

Collaborate with us on GitHub
The source for this content can be found on GitHub, where you can also create and review issues and pull requests. For more information, seeour contributor guide.

Feedback

Was this page helpful?

YesNo

In this article

Was this page helpful?

YesNo