次の方法で共有


レコード​​

メモ

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

機能の仕様と行われた実装では、いくつかの違いがあることがあります。 これらの違いは、関連する言語設計ミーティング (LDM) メモに取り上げられています。

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

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

この提案では、C# 言語設計チームが同意した C# 9 レコード機能の仕様を追跡します。

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

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_declarationparameter_list が含まれていない場合、record_baseargument_list を指定するレコードのエラーとなります。 部分レコードの最大の partial 型宣言の 1 つは、parameter_list です。

レコード パラメータは、refout、または this 修飾子を使用できませんが、inparams は使用できます。

継承

レコードはクラスが objectでない限り、クラスから継承することはできませんし、クラスもレコードから継承することはできません。 レコードは他のレコードから継承できます。

レコード型のメンバー

レコード本文で宣言されたメンバーに加え、レコード型には、追加の合成メンバーがあります。 メンバーは、「照合」シグネチャを持つメンバーがレコード本文で宣言されていない限り、または「照合」シグネチャを持つアクセス可能な具体的な非仮想メンバーが継承されない限り、合成されます。 一致するメンバーは、他の合成されたメンバーではなく、コンパイラがそのメンバーを生成できないようにします。 2 つのメンバーが同じシグネチャを持っている場合、または継承シナリオで「非表示」と見なされる場合、2 つのメンバーは一致と見なされます。 レコードのメンバーの名前を "Clone" にすることはエラーです。 レコードのインスタンス フィールドに最上位のポインター型があると、エラーになります。 ネストされたポインター型 (ポインターの配列など) を使用できます。

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

等値メンバー

レコードが、object から派生した場合、レコード型には、次のように宣言されたプロパティと同等の合成された読み取り専用のプロパティが含まれます。

Type EqualityContract { get; }

レコード型が sealed の場合、プロパティは、private になります。 それ以外の場合、プロパティは virtual および protectedです。 プロパティは明示的に宣言できます。 明示的な宣言が予期されるシグネチャと一致しない場合、または明示的な宣言で、派生型でオーバーライドが許可されておらず、レコード型が sealed ではない場合、エラーとなります。

レコード型が、基本レコード型 Base から派生した場合、レコード型には、次のように宣言されたプロパティと同等の合成された読み取り専用のプロパティが含まれます。

protected override Type EqualityContract { get; }

プロパティは明示的に宣言できます。 明示的な宣言が予期されるシグネチャと一致しない場合、または明示的な宣言で、派生型でオーバーライドが許可されておらず、レコード型が sealed ではない場合、エラーとなります。 合成されたプロパティまたは明示的に宣言されたプロパティが、レコード型 Base でシグネチャがあるプロパティをオーバーライドしない場合は (例えば、プロパティが Base に存在しない、シールされている、または仮想ではない場合など)、エラーになります。 合成されたプロパティは、レコードの種類が typeof(R) であるとき、R を返します。

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

Equals(R? other) が、ユーザー定義で (合成されていない)、GetHashCode がそうでない場合は、警告が表示されます。

public virtual bool Equals(R? other);

次のいずれかが true の場合のみ、合成された Equals(R?) は、true を返します。

  • othernullではない
  • 継承されていないレコード型内の各インスタンス フィールド fieldN についての、System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN) の値 (TN はフィールド型)
  • 基本レコード型がある場合は、base.Equals(other) の値 (public virtual bool Equals(Base? other) への非仮想呼び出し) となり、そうでない場合は、EqualityContract == other.EqualityContract の値です。

レコード型には、合成された ==!= の演算子があります。この演算子は、次のように宣言された演算子と同等です。

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

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

レコード型が、基本レコード型 Base から派生した場合、レコード型には、次のように宣言されたメソッドと同等の合成されたオーバーライドが含まれます。

public sealed override bool Equals(Base? other);

オーバーライドが明示的に宣言されている場合は、エラーになります。 メソッドがレコード型 Base で同じシグネチャを持つメソッドをオーバーライドしない場合(たとえば、メソッドが Baseに存在しない場合や、シールされている場合、または仮想メソッドでない場合など)はエラーです。 合成されたオーバーライドは、Equals((object?)other) を返します。

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

public override bool Equals(object? obj);

オーバーライドが明示的に宣言されている場合は、エラーになります。 メソッドが object.Equals(object? obj) をオーバーライドしない場合は (たとえば、中間基本型でのシャドウイングなど)、エラーになります。 合成されたオーバーライドは、レコードの種類が Equals(other as R) の場合に R を返します。

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

public override int GetHashCode();

メソッドは明示的に宣言できます。 明示的な宣言で派生型でのオーバーライドが許可されておらず、レコード型が sealed でない場合は、エラーになります。 合成されたメソッド、または明示的に宣言されたメソッドのいずれかが object.GetHashCode() をオーバーライドしない場合は (たとえば、中間基本型でのシャドウイングなど)、エラーとなります。

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

GetHashCode() の合成されたオーバーライドは、次の値を組み合わせた int の結果を返します。

  • 継承されていないレコード型内の各インスタンス フィールド fieldN についての、System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN) の値 (TN はフィールド型)
  • 基本レコード型がある場合は、base.GetHashCode() の値、それ以外の場合は、System.Collections.Generic.EqualityComparer<System.Type>.Default.GetHashCode(EqualityContract) の値です。

たとえば、次のようなレコード型があるとします。

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

これらのレコード型の場合、合成された等価メンバーは次のようになります。

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

メンバーをコピーおよびクローンする

レコード型には、次の 2 つのコピー メンバーが含まれています。

  • レコード型の 1 つの引数を受け取るコンストラクタ。 これは、「コピー コンストラクタ」と呼ばれます。
  • コンパイラによって予約された名前を持つ、合成されたパブリック パラメーターなしのインスタンス「clone」メソッド

コピー コンストラクタの目的は、パラメーターの state を作成された新しいインスタンスにコピーすることです。 このコンストラクタは、レコード宣言に存在するインスタンス フィールド/プロパティ 初期化子を実行しません。 コンストラクタが明示的に宣言されていない場合、コンストラクタはコンパイラによって合成されます。 レコードがシールされている場合、コンストラクタはプライベートになり、それ以外の場合は保護されます。 明示的に宣言されたコピー コンストラクタは、レコードがシールされていない限り、パブリックにするかまたは保護されている必要があります。 コンストラクタが最初に行う必要があるのは、基本のコピー コンストラクタを呼び出すか、レコードがオブジェクトから継承する場合は、パラメーターなしのオブジェクト コンストラクタを呼び出すことです。 ユーザー定義のコピー コンストラクタで、この要件を満たしていない暗黙的または明示的なコンストラクタ 初期化子が使用されている場合、エラーが報告されます。 基本コピー コンストラクタが呼び出されると、合成コピー コンストラクタは、レコード型内で暗黙的または明示的に宣言されたすべてのインスタンス フィールドの値をコピーします。 明示的か暗黙的かに関係なく、コピー コンストラクタが唯一存在しても、既定のインスタンス コンストラクタが自動的に追加されるのを防ぐことはありません。

基本レコードに仮想「clone」メソッドが存在する場合、合成された「clone」メソッドによってオーバーライドされ、メソッドの戻り値の型が現在の包含型になります。 基本レコード clone メソッドがシールされている場合、エラーが生成されます。 仮想「clone」メソッドが基本レコードに存在しない場合、レコードがシールされていないまたは抽象でない限り、clone メソッドの戻り値の型は包含型であり、メソッドは仮想です。 含まれるレコードが抽象の場合、合成された clone メソッドも抽象です。 「clone」メソッドが抽象でない場合は、コピー コンストラクタの呼び出しの結果を返します。

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

レコードが object から派生した場合、レコードには、次のように宣言されたメソッドと同等の合成メソッドが含まれます。

bool PrintMembers(System.Text.StringBuilder builder);

レコード型が private の場合、メソッドは sealed です。 それ以外の場合、メソッドは virtualprotectedになります。

このメソッドでは、次の操作を行います。

  1. メソッドが存在し、レコードに印刷可能なメンバーがある場合は、メソッド System.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack() を呼び出します。
  2. レコードの印刷可能な各メンバー(非静的なパブリックフィールドと読み取り可能なプロパティメンバー)について、まずそのメンバーの名前の後に " = " を追加し、その後にメンバーの値を追記します。そして各メンバーのペアを ", " で区切ります。
  3. レコードに印刷可能なメンバーがある場合は true を返します。

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

レコード型が、基本レコード Base から派生した場合、レコードには、次のように宣言されたメソッドと同等の合成されたオーバーライドが含まれます。

protected override bool PrintMembers(StringBuilder builder);

レコードに印刷可能なメンバーがない場合、メソッドは 1 つの引数 (builder パラメーター) を使用して基本 PrintMembers メソッドを呼び出し、結果を返します。

それ以外の場合、メソッドは次のようになります。

  1. 1 つの引数 (builder パラメーター) を使用して基本 PrintMembers メソッドを呼び出します。
  2. PrintMembers メソッドが true を返した場合は、ビルダーに「,」を追加します。
  3. レコードの印刷可能なメンバーごとに、そのメンバーの名前の後に「=」を追加し、その後にメンバーの値 this.member (または値型の場合は this.member.ToString()) を追加し、「"」で区切ります。
  4. true を返します。

PrintMembers メソッドは明示的に宣言できます。 明示的な宣言が予期されるシグネチャと一致しない場合、または明示的な宣言で、派生型でオーバーライドが許可されておらず、レコード型が sealed ではない場合、エラーとなります。

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

public override string ToString();

メソッドは明示的に宣言できます。 明示的な宣言が予期されるシグネチャと一致しない場合、または明示的な宣言で、派生型でオーバーライドが許可されておらず、レコード型が sealed ではない場合、エラーとなります。 合成されたメソッド、または明示的に宣言されたメソッドのいずれかが object.ToString() をオーバーライドしない場合は (たとえば、中間基本型でのシャドウイングなど)、エラーとなります。

合成されたメソッド:

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

たとえば、次のようなレコード型があるとします。

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

これらのレコード型の場合、合成された出力メンバーは次のようになります。

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

位置指定レコード メンバー

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

プライマリ コンストラクター

レコード型には、型宣言の値パラメーターに対応するシグネチャを持つパブリック コンストラクタがあります。 これは型のプライマリ コンストラクタと呼ばれ、暗黙的に宣言された既定のクラス コンストラクタ (存在する場合) が抑制されます。 プライマリ コンストラクタと同じシグネチャを持つコンストラクタが既にクラスに存在するのはエラーです。

実行時にプライマリ コンストラクターで、以下を行います

  1. クラス本文に表示されるインスタンス 初期化子を実行します。

  2. record_base 句で指定された引数を使用して基底クラス コンストラクタを呼び出します (存在する場合)

レコードにプライマリ コンストラクタがある場合、「コピー コンストラクター」を除くユーザー定義コンストラクタには、明示的な this コンストラクター 初期化子が必要です。

プライマリ コンストラクターのパラメーターとレコードのメンバーは、record_base 句の argument_list 内、およびインスタンス フィールドまたはプロパティの初期化子の範囲内にあります。 インスタンス メンバーは、これらの場所ではエラーを引き起こします。これは、今日の通常のコンストラクター初期化子におけるインスタンス メンバーのスコープ内でのエラーに似ていますが、使用するとエラーになります。これに対して、プライマリ コンストラクターのパラメーターはスコープ内にあり、使用可能であり、メンバーをオーバーシャドウします。 現在の通常のコンストラクタでの基本呼び出しと初期化子の動作と同様に、静的メンバーも使用できます。

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

argument_list で宣言された式変数は、argument_list 内の範囲にあります。 通常のコンストラクタ 初期化子の引数リスト内と同じシャドーイング ルールが適用されます。

プロパティ

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

レコードの場合:

  • パブリック getinit 自動プロパティが作成されます (別の init アクセサの仕様を参照)。 型が一致する継承された abstract プロパティはオーバーライドされます。 継承されたプロパティに public オーバーライド可能な getinit アクセサーがない場合はエラーです。 継承されたプロパティが非表示の場合は、エラーになります。
    自動プロパティは、対応するプライマリ コンストラクタ パラメーターの値に初期化されます。 合成された自動プロパティとそのバッキング フィールドに属性を適用するには、対応するレコード パラメーターに構文的に適用される属性の property: または field: ターゲットを使用します。

分解

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

次の例は、Deconstruct に合成されたコンパイラがある位置指定レコード R とその使用方法を示しています。

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

with 式は、次の構文を使用する新しい式です。

with_expression
    : switch_expression
    | switch_expression 'with' '{' member_initializer_list? '}'
    ;

member_initializer_list
    : member_initializer (',' member_initializer)*
    ;

member_initializer
    : identifier '=' expression
    ;

with 式は、ステートメントとして認められていません。

with 式を使用すると、member_initializer_list 代入変更に加えてレシーバー式のコピーを生成するように設計された「非破壊的変異」が許可されます。

有効な with 式には、void 以外の型を持つレシーバーがあります。 受信型はレコードである必要があります。

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

まず、レシーバーの「clone」メソッド (上記で指定) が呼び出され、その結果がレシーバーの型に変換されます。 次に、各 member_initializer は、変換の結果のフィールドまたはプロパティ アクセスへの代入と同じ方法で処理されます。 代入は、字句順に処理されます。