次の方法で共有


レコード構造体

手記

この記事は機能仕様です。 仕様は、機能の設計ドキュメントとして機能します。 これには、提案された仕様の変更と、機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が最終決定され、現在の ECMA 仕様に組み込まれるまで公開されます。

機能の仕様と完成した実装の間には、いくつかの違いがある可能性があります。 これらの違いは、関連する 言語設計会議 (LDM) ノートでキャプチャされます。

機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。

チャンピオンの課題: https://github.com/dotnet/csharplang/issues/4334

レコード構造体の構文は次のとおりです。

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

レコード構造体型は、他の構造体型と同様に値型です。 クラス System.ValueTypeから暗黙的に継承されます。 レコード構造体の修飾子とメンバーには、構造体と同じ制限が適用されます (型のアクセシビリティ、メンバーの修飾子、base(...) インスタンス コンストラクター初期化子、コンストラクター内の this の明確な割り当て、デストラクター、...)。レコード構造体は、パラメーターなしのインスタンス コンストラクターとフィールド初期化子の構造体と同じ規則にも従いますが、このドキュメントでは、通常、構造体に対する制限を解除することを前提としています。

§16.4.9 パラメーターなしの構造体コンストラクター 仕様 参照してください。

レコード構造体では、修飾子 ref 使用できません。

部分レコード構造体の最大 1 つの部分型宣言は、parameter_listを提供できます。 parameter_list は空である可能性があります。

レコード構造体パラメーターでは、refout、または this 修飾子を使用できません (ただし、in および params は許可されます)。

レコード構造体のメンバー

レコード構造体本体で宣言されたメンバーに加えて、レコード構造体型には合成メンバーが追加されています。 メンバーは、"照合" 署名を持つメンバーがレコード構造体本体で宣言されていない限り、または "照合" 署名を持つアクセス可能な具象の非仮想メンバーが継承されない限り、合成されます。 2 つのメンバーが同じシグネチャを持っている場合、または継承シナリオで "非表示" と見なされる場合、2 つのメンバーは一致と見なされます。 「シグネチャとオーバーロード」 (§7.6) を参照してください。 "レコード構造体のメンバーに「Clone」と名付けることはエラーです。"

レコード構造体のインスタンス フィールドが安全でない型を持つことはエラーです。

レコード構造体はデストラクターを宣言できません。

合成されるメンバーは次のとおりです。

等値メンバー

この型の合成された等価メンバーは、レコードクラスのものと同様です(この型のEqualsobject 型の Equals、およびこの型の ==!= 演算子)。
ただし、EqualityContract、null チェック、継承がない点が異なります。

レコード構造体は System.IEquatable<R> を実装しており、合成され厳密に型指定されたEquals(R other) のオーバーロードを含みます (R はレコード構造体)。 メソッドは publicです。 メソッドは明示的に宣言できます。 明示的な宣言が想定されるシグネチャまたはアクセシビリティと一致しない場合はエラーです。

Equals(R other) がユーザー定義 (合成されていない) のに GetHashCode されていない場合は、警告が生成されます。

public readonly bool Equals(R other);

合成された Equals(R) は、レコード構造体の各インスタンス フィールド fieldN に対して、System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN) (TN はフィールドの型) の値が true である場合にのみ true を返します。

レコード構造体には、次のように宣言された演算子と同等の合成 == 演算子と != 演算子が含まれます。

public static bool operator==(R r1, R r2)
    => r1.Equals(r2);
public static bool operator!=(R r1, R r2)
    => !(r1 == r2);

== 演算子によって呼び出される Equals メソッドは、上記で指定した Equals(R other) メソッドです。 != 演算子は、== 演算子にデリゲートします。 演算子が明示的に宣言されている場合はエラーです。

レコード構造体には、次のように宣言されたメソッドと同等の合成オーバーライドが含まれています。

public override readonly bool Equals(object? obj);

オーバーライドが明示的に宣言されている場合はエラーです。 合成されたオーバーライドは、R がレコード構造体である場合に other is R temp && Equals(temp) を返します。

レコード構造体には、次のように宣言されたメソッドと同等の合成オーバーライドが含まれています。

public override readonly int GetHashCode();

メソッドは明示的に宣言できます。

いずれかの Equals(R)GetHashCode() が明示的に宣言されているが、他のメソッドが明示的でない場合、警告が報告されます。

合成されたオーバーライドである GetHashCode() は、各インスタンスフィールド fieldN に対する System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN) の値を、fieldNの型である TN と組み合わせた結果として int を返します。

たとえば、次のレコード構造体を考えてみましょう。

record struct R1(T1 P1, T2 P2);

このレコード構造体では、合成された等値メンバーは次のようになります。

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

メンバーの出力: PrintMembers メソッドと ToString メソッド

レコード構造体には、次のように宣言されたメソッドと同等の合成メソッドが含まれています。

private bool PrintMembers(System.Text.StringBuilder builder);

このメソッドは次の処理を行います。

  1. レコード構造体の印刷可能なメンバー (非静的パブリック フィールドと読み取り可能なプロパティ メンバー) ごとに、そのメンバーの名前の後に " = " を付加し、その後にメンバーの値を付加して ", " で区切ります。
  2. レコード構造体に印刷可能なメンバーがある場合は true を返します。

値型を持つメンバーの場合、ターゲット プラットフォームで使用できる最も効率的なメソッドを使用して、その値を文字列形式に変換します。 現時点では、StringBuilder.Appendに渡す前に ToString を呼び出すことを意味します。

レコードの印刷可能なメンバーに、readonlyget 以外のアクセサーを持つ読み取り可能なプロパティが含まれていない場合、合成された PrintMembersreadonlyPrintMembers メソッドを readonly にするために、レコードのフィールドを readonly にする必要はありません。

PrintMembers メソッドは明示的に宣言できます。 明示的な宣言が想定されるシグネチャまたはアクセシビリティと一致しない場合はエラーです。

レコード構造体には、次のように宣言されたメソッドと同等の合成メソッドが含まれています。

public override string ToString();

レコード構造体の PrintMembers メソッドが readonly場合、合成された ToString() メソッドは readonly

メソッドは明示的に宣言できます。 明示的な宣言が想定されるシグネチャまたはアクセシビリティと一致しない場合はエラーです。

合成されたメソッド:

  1. StringBuilder インスタンスを作成します。
  2. レコード構造体名をビルダーに追加し、その後に " { " を追加します。
  3. レコード構造体の PrintMembers メソッドを呼び出してビルダーを渡し、true が返された場合は続けて " " を渡します。
  4. "}"を追加します
  5. builder.ToString() でビルダーの内容を返します。

たとえば、次のレコード構造体を考えてみましょう。

record struct R1(T1 P1, T2 P2);

このレコード構造体では、合成された印刷メンバーは次のようになります。

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

位置レコード構造体のメンバー

上記のメンバーに加えて、パラメーター リスト ("位置指定レコード") を持つレコード構造体は、上記のメンバーと同じ条件を持つ追加のメンバーを合成します。

主コンストラクター

レコード構造体には、型宣言の値パラメーターに対応するシグネチャを持つパブリック コンストラクターがあります。 これは、型のプライマリ コンストラクターと呼ばれます。 プライマリ コンストラクターと同じシグネチャを持つコンストラクターが構造体に既に存在するのはエラーです。 型宣言にパラメーター リストが含まれていない場合、プライマリ コンストラクターは生成されません。

record struct R1
{
    public R1() { } // ok
}

record struct R2()
{
    public R2() { } // error: 'R2' already defines constructor with same parameter types
}

レコード構造体のインスタンス フィールド宣言には、変数初期化子を含めることができます。 プライマリ コンストラクターがない場合、インスタンス初期化子はパラメーターなしのコンストラクターの一部として実行されます。 それ以外の場合、実行時にプライマリ コンストラクターは、レコード構造体本体に出現するインスタンス初期化子を実行します。

レコード構造体にプライマリ コンストラクターがある場合、ユーザー定義コンストラクターには、プライマリ コンストラクターまたは明示的に宣言されたコンストラクターを呼び出す明示的な this コンストラクター初期化子が必要です。

プライマリ コンストラクターのパラメーターとレコード構造体のメンバーは、インスタンス フィールドまたはプロパティの初期化子内のスコープ内にあります。 インスタンス メンバーは、これらの場所で使用するとエラーになります (これは、通常のコンストラクター初期化子におけるインスタンス メンバーのスコープ内の状況と類似していますが、使用すること自体がエラーとなります)。しかし、プライマリ コンストラクターのパラメーターはスコープ内で使用可能で、それがメンバーを覆い隠します。 静的メンバーも使用可能です。

プライマリ コンストラクターのパラメーターが読み取られない場合、警告が生成されます。

構造体インスタンス コンストラクターの明確な代入規則は、レコード構造体のプライマリ コンストラクターに適用されます。 たとえば、次のようなエラーが発生します。

record struct Pos(int X) // definite assignment error in primary constructor
{
    private int x;
    public int X { get { return x; } set { x = value; } } = X;
}

プロパティ

レコード構造体宣言の各レコード構造体パラメーターには、値パラメーター宣言から名前と型を取得する、対応するパブリック プロパティ メンバーがあります。

レコード構造体の場合:

  • レコード構造体に修飾子が readonly の場合、パブリック get および init の自動プロパティが作成され、そうでない場合は get または set が作成されます。 セット アクセサー (setinit) の両方が "一致" と見なされます。 そのため、ユーザーは、合成された変更可能なプロパティの代わりに init のみのプロパティを宣言できます。 型が一致する継承された abstract プロパティはオーバーライドされます。 レコード構造体に名前と型が必要なインスタンス フィールドがある場合、自動プロパティは作成されません。 継承されたプロパティに publicget および set/init アクセサーがない場合はエラーです。 継承されたプロパティまたはフィールドが非表示の場合はエラーです。
    自動プロパティは、対応するプライマリ コンストラクター パラメーターの値に初期化されます。 合成された自動プロパティとそのバッキング フィールドに属性を適用するには、対応するレコード構造体パラメーターに構文的に適用される属性の property: または field: ターゲットを使用します。

分解

少なくとも 1 つのパラメーターを持つ位置レコード構造体は、プライマリ コンストラクター宣言の各パラメーターに対する out パラメーター宣言を使用して、Deconstruct 呼び出されたパブリック void を返すインスタンス メソッドを合成します。 Deconstruct メソッドの各パラメーターは、プライマリ コンストラクター宣言の対応するパラメーターと同じ型を持ちます。 メソッドの本体は、Deconstruct メソッドの各パラメーターを、インスタンス メンバーから同じ名前のメンバーへのアクセスの値に割り当てます。 本文でアクセスされるインスタンス メンバーに、readonlyget 以外のアクセサーを持つプロパティが含まれていない場合、合成された Deconstruct メソッドは readonly。 メソッドは明示的に宣言できます。 明示的な宣言が予期されるシグネチャまたはアクセシビリティと一致しない場合、または静的な場合はエラーです。

構造体での with 式を許可する

これで、with 式の受信者が構造体型を持つことが有効になりました。

with 式の右側には、識別子への割り当てのシーケンスを含む member_initializer_list があり、これは受信側の型のアクセス可能なインスタンス フィールドまたはプロパティである必要があります。

構造体型の受信者の場合、最初に受信者がコピーされ、次に各 member_initializer が変換の結果のフィールドまたはプロパティ アクセスへの割り当てと同じ方法で処理されます。 割り当ては字句の順序で処理されます。

レコードの機能強化

record class を許可する

レコード型の既存の構文では、recordと同じ意味を持つ record class が許可されます。

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

ユーザー定義の位置指定メンバーをフィールドにすることを許可する

https://github.com/dotnet/csharplang/blob/master/meetings/2020/LDM-2020-10-05.md#changing-the-member-type-of-a-primary-constructor-parameter を参照してください

レコードに名前と型が必要なインスタンス フィールドがある場合、または継承されている場合、自動プロパティは作成されません。

構造体でパラメーターなしのコンストラクターとメンバー初期化子を許可する

パラメーターなしの構造体コンストラクター 仕様 参照してください。

質問を開く

  • メタデータ内のレコード構造体を認識する方法 (未発表の活用可能なクローン メソッドはありません...)

回答しました

  • PrintMembers のデザインを維持することを確認します (別のメソッドは boolを返します) (回答: はい)
  • record ref struct (IEquatable<RefStruct> フィールドと ref フィールドに関する問題) を許可しないことを確認します (回答: はい)
  • 等価メンバーの実装が確認されました。 別の方法は、合成された bool Equals(R other)bool Equals(object? other)、演算子がすべて ValueType.Equalsに委任されるということです。 (回答: はい)
  • プライマリ コンストラクターがある場合にフィールド初期化子を許可することを確認します。 また、(アクティベーターの問題が明らかに修正されました)、パラメーターなしの構造体コンストラクターを許可しますか? (回答: はい、更新された仕様は LDM で確認する必要があります)
  • Combine 方法についてどのくらい言いたいですか? (回答:可能な限り少ない)
  • コピー コンストラクターシグネチャを持つユーザー定義コンストラクターを禁止する必要がありますか? (答え:いいえ、レコード構造体スペックにコピーコンストラクターの概念はありません)
  • "Clone" という名前のメンバーを禁止することを確認します。 (回答: 正解)
  • 合成された Equals ロジックが機能的にランタイム実装 (float など) と同等であることを再確認します。NaN) (回答: LDM で確認済み)
  • フィールドまたはプロパティターゲット属性を位置指定パラメーター リストに配置できますか? (回答: はい、レコード クラスの場合と同じ)
  • ジェネリックで with は使用できるか? (回答: C# 10 の範囲外)
  • GetHashCoderecord struct S1;record struct S2;の間で異なる値を取得するために、型自体のハッシュを含める必要がありますか? (回答: いいえ)