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

Record structs

  • 2023-06-23
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/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 classSystem.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 forthis 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.9Seeparameterless struct constructors spec.

Record structs cannot useref modifier.

At most one partial type declaration of a partial record struct may provide aparameter_list.Theparameter_list may be empty.

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

Members of a record struct

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 oran accessible concrete non-virtual member with a "matching" signature is inherited.Two members are considered matching if they have the samesignature 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:

Equality members

The synthesized equality members are similar as in a record class (Equals for this type,Equals forobject type,== and!= operators for this type),
except for the lack ofEqualityContract, null checks or inheritance.

The record struct implementsSystem.IEquatable<R> and includes a synthesized strongly-typed overload ofEquals(R other) whereR is the record struct.The method ispublic.The method can be declared explicitly. It is an error if the explicit declaration does not match the expected signature or accessibility.

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

public readonly bool Equals(R other);

The synthesizedEquals(R) returnstrue if and only if for each instance fieldfieldN in the record structthe value ofSystem.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN) whereTN is the field type istrue.

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

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.

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 returnsother is R temp && Equals(temp) whereR 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 ofEquals(R) andGetHashCode() is explicitly declared but the other method is not explicit.

The synthesized override ofGetHashCode() returns anint result of combining the values ofSystem.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN) for each instance fieldfieldN withTN being the type offieldN.

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));    }}

Printing members: PrintMembers and ToString methods

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:

  1. for each of the record struct'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 ", ",
  2. return true if the record struct 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's printable members do not include a readable property with a non-readonlyget accessor, then the synthesizedPrintMembers isreadonly. There is no requirement for the record's fields to bereadonly for thePrintMembers method to bereadonly.

ThePrintMembers 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'sPrintMembers method isreadonly, then the synthesizedToString() method isreadonly.

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:

  1. creates aStringBuilder instance,
  2. appends the record struct name to the builder, followed by " { ",
  3. invokes the record struct'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 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();    }}

Positional record struct members

In addition to the above members, record structs with a parameter list ("positional records") synthesizeadditional members with the same conditions as the members above.

Primary Constructor

A record struct has a public constructor whose signature corresponds to the value parameters of thetype declaration. This is called the primary constructor for the type. It is an error to have a primaryconstructor 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 anexplicitthis 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 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.

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 followingis 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;}

Properties

For each record struct parameter of a record struct declaration there is a corresponding public propertymember whose name and type are taken from the value parameter declaration.

For a record struct:

  • A publicget andinit auto-property is created if the record struct hasreadonly modifier,get andset otherwise.Both kinds of set accessors (set andinit) are considered "matching". So the user may declare an init-only propertyin place of a synthesized mutable one.An inheritedabstract 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 havepublicget andset/init accessors.It is an error if the inherited property or field 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 struct parameter.

Deconstruct

A positional record struct with at least one parameter synthesizes a public void-returning instance method calledDeconstruct with an outparameter declaration for each parameter of the primary constructor declaration. Each parameterof the Deconstruct method has the same type as the corresponding parameter of the primaryconstructor declaration. The body of the method assigns each parameter of the Deconstruct methodto 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 witha non-readonlyget accessor, then the synthesizedDeconstruct method isreadonly.The method can be declared explicitly. It is an error if the explicit declaration does not matchthe expected signature or accessibility, or is static.

Allowwith expression on structs

It is now valid for the receiver in awith expression to have a struct type.

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.

For a receiver with struct type, the receiver is first copied, then eachmember_initializer is processedthe same way as an assignment to a field or property access of the result of the conversion.Assignments are processed in lexical order.

Improvements on records

Allowrecord class

The existing syntax for record types allowsrecord class with the same meaning asrecord:

record_declaration    : attributes? class_modifier* 'partial'? 'record' 'class'? identifier type_parameter_list?      parameter_list? record_base? type_parameter_constraints_clause* record_body    ;

Allow user-defined positional members to be fields

Seehttps://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-10-05.md#changing-the-member-type-of-a-primary-constructor-parameter

No auto-property is created if the record has or inherits an instance field with expected name and type.

Allow parameterless constructors and member initializers in structs

Seeparameterless struct constructors spec.

Open questions

  • how to recognize record structs in metadata? (we don't have an unspeakable clone method to leverage...)

Answered

  • confirm that we want to keep PrintMembers design (separate method returningbool) (answer: yes)
  • confirm we won't allowrecord ref struct (issue withIEquatable<RefStruct> and ref fields) (answer: yes)
  • confirm implementation of equality members. Alternative is that synthesizedbool Equals(R other),bool Equals(object? other) and operators all just delegate toValueType.Equals. (answer: yes)
  • confirm that we want to allow field initializers when there is a primary constructor. Do we also want to allow parameterless struct constructors while we're at it (the Activator issue was apparently fixed)? (answer: yes, updated spec should be reviewed in LDM)
  • how much do we want to say aboutCombine method? (answer: as little as possible)
  • should we disallow a user-defined constructor with a copy constructor signature? (answer: no, there is no notion of copy constructor in the record structs spec)
  • confirm that we want to disallow members named "Clone". (answer: correct)
  • double-check that synthesizedEquals logic is functionally equivalent to runtime implementation (e.g. float.NaN) (answer: confirmed in LDM)
  • could field- or property-targeting attributes be placed in the positional parameter list? (answer: yes, same as for record class)
  • with on generics? (answer: out of scope for C# 10)
  • shouldGetHashCode include a hash of the type itself, to get different values betweenrecord struct S1; andrecord struct S2;? (answer: no)
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