次の方法で共有


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

メモ

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

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

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

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

まとめ

クラスと構造体にはパラメーター リストを定義でき、基底クラスの指定部分には引数リストを含めることができます。 プライマリ コンストラクター パラメーターのスコープは、クラスまたは構造体の宣言全体であり、それらのパラメーターが関数メンバーや無名関数によって取得された場合、適切に格納されます (たとえば、宣言されたクラスや構造体の非公開フィールドとして管理されます)。

この提案は、合成されたいくつかの追加メンバーを備えたこのより一般的な機能に関して、記録上ですでに利用可能な主要なコンストラクターを「再考」しています。

目的

C# においてクラスや構造体に複数のコンストラクターを定義できると、クラスや構造体の汎用性が高まりますが、その分、宣言の構文が冗長になるという欠点があります。これは、コンストラクターの入力とクラスの状態を明確に分離する必要があるためです。

プライマリ コンストラクター パラメーターのスコープは、クラスまたは構造体全体であり、初期化やオブジェクトの状態管理に直接使用できます。 ただし、他のコンストラクターは必ずプライマリ コンストラクターを介して呼び出すという制約があります。

public class B(bool b) { } // base class

public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(S));
    }
    public C(string s) : this(true, 0, s) { } // must call this(...)
}

詳細な設計

この仕様では、レコード型と非レコード型にまたがる一般的な設計を説明しており、プライマリ コンストラクターが存在する場合に、合成メンバーを追加することで、レコード型におけるプライマリ コンストラクターがどのように指定されるかを詳しく説明しています。

構文

クラスや構造体の宣言が拡張され、型名にパラメーター リストを、基底クラスに引数リストを記述できるようになり、それによって本体は単に ;: から構成される形になります。

class_declaration
  : attributes? class_modifier* 'partial'? class_designator identifier type_parameter_list?
  parameter_list? class_base? type_parameter_constraints_clause* class_body
  ;
  
class_designator
  : 'record' 'class'?
  | 'class'
  
class_base
  : ':' class_type argument_list?
  | ':' interface_type_list
  | ':' class_type  argument_list? ',' interface_type_list
  ;  
  
class_body
  : '{' class_member_declaration* '}' ';'?
  | ';'
  ;
  
struct_declaration
  : attributes? struct_modifier* 'partial'? 'record'? 'struct' identifier type_parameter_list?
    parameter_list? struct_interfaces? type_parameter_constraints_clause* struct_body
  ;

struct_body
  : '{' struct_member_declaration* '}' ';'?
  | ';'
  ;
  
interface_declaration
  : attributes? interface_modifier* 'partial'? 'interface'
    identifier variant_type_parameter_list? interface_base?
    type_parameter_constraints_clause* interface_body
  ;  
    
interface_body
  : '{' interface_member_declaration* '}' ';'?
  | ';'
  ;

enum_declaration
  : attributes? enum_modifier* 'enum' identifier enum_base? enum_body
  ;

enum_body
  : '{' enum_member_declarations? '}' ';'?
  | '{' enum_member_declarations ',' '}' ';'?
  | ';'
  ;

注: これらの製品は、record_declaration および record_struct_declaration を置き換え、いずれも廃止されます。

なお、class_baseargument_list があり、外側の class_declarationparameter_list がない場合、エラーになります。 また、部分クラスまたは部分構造体の部分型宣言のうち、parameter_list を定義できるのは最大で 1 つの宣言のみです。 record 宣言の parameter_list に含まれるパラメーターはすべて値パラメーターである必要があります。

なお、この提案によると、class_bodystruct_bodyinterface_bodyenum_body; のみで構成できます。

parameter_list を持つクラスまたは構造体には、型宣言の値パラメーターと一致するシグネチャを持つ暗黙的な public コンストラクターが生成されます。 これは型のプライマリ コンストラクターと呼ばれ、暗黙的に宣言されたパラメーター コンストラクター (存在する場合) が抑制されます。 プライマリ コンストラクターと同じシグネチャを持つコンストラクターが既に型宣言に存在するのはエラーです。

参照

単純な名前の検索は、プライマリ コンストラクター パラメーターを処理するように拡張されています。 変更点は、次の抜粋で太字で強調されています。

  • それ以外の場合は、各インスタンス型 T (§15.3.2) に対して、すぐ外側の型宣言のインスタンス型から始まり、外側の各クラスまたは構造体宣言のインスタンス型 (存在する場合) を続行します。
    • T の宣言にプライマリ コンストラクター パラメーター I が含まれており、class_baseTargument_list 内、または T のフィールド、プロパティ、イベントの初期化子内で参照が発生した場合、その参照はプライマリ コンストラクター パラメーター I を指します。
    • それ以外で、e が 0 で、T の宣言に名前 I を持つ型パラメーターが含まれている場合、simple_name はその型パラメーターを参照します。
    • それ以外の場合、e 型引数を持つ T 内の I のメンバー参照 (§12.5) が一致を生成する場合:
      • T が、すぐ外側のクラスまたは構造体型のインスタンス型であり、検索で 1 つ以上のメソッドが識別される場合、結果は、 thisのインスタンス式が関連付けられたメソッド グループになります。 型引数リストが指定されている場合は、ジェネリック メソッド (§12.8.10.2) の呼び出しで使用されます。
      • それ以外の場合、 T がすぐ外側のクラスまたは構造体のインスタンスの型であり、検索がインスタンス メンバーを識別し、参照がインスタンス コンストラクター、インスタンス メソッド、またはインスタンス アクセサーの ブロック 内で発生する場合 (§12.2.1)、結果は形式 this.Iのメンバー アクセス (§12.8.7) と同じになります。 これは、 e が 0 の場合にのみ発生します。
      • それ以外の場合、結果はフォーム T.I または T.I<A₁, ..., Aₑ>のメンバー アクセス (§12.8.7) と同じです。
    • それ以外の場合、T の宣言にプライマリ コンストラクター パラメーター I が含まれていると、結果はプライマリ コンストラクター パラメーター I になります。

最初の変更点は、レコード型に対するプライマリ コンストラクターの影響を示し、フィールドの初期化子や基底クラスの引数リスト内でプライマリ コンストラクター パラメーターが確実に優先されるようになります。 この規則は静的初期化子にも拡張されます。 ただし、レコード型ではパラメーターと同じ名前のインスタンス メンバーが常に存在するため、この拡張によって変更されるのはエラー メッセージの内容のみです。 パラメーターへの不正なアクセスか、インスタンス メンバーへの不正なアクセスかの違いです。

2 つ目の変更点は、プライマリ コンストラクター パラメーターが型本体の他の場所でも参照できることを示していますが、これはメンバーによって隠蔽されていない場合に限ります。

プライマリ コンストラクター パラメーターを参照する場合は、以下のいずれかの条件を満たさないと、エラーになります。

  • nameof 引数
  • 宣言型 (パラメーターを持つプライマリ コンストラクターを宣言する型) のインスタンス フィールド、プロパティ、イベントの初期化子内。
  • 宣言型の class_baseargument_list
  • 宣言型のインスタンス メソッド (インスタンス コンストラクターは除く) の本体内。
  • 宣言されている型に属するインスタンスアクセサーの本体。

つまり、プライマリ コンストラクター パラメーターのスコープは宣言型の本体全体となります。 宣言型のメンバーを、宣言型のフィールド、プロパティ、またはイベントの初期化子内、または宣言型の class_baseargument_list 内でシャドウします。 他の場所では宣言型のメンバーによってシャドウされます。

たとえば、次のような宣言があるとします。

class C(int i)
{
    protected int i = i; // references parameter
    public int I => i; // references field
}

この場合、フィールド i の初期化子ではパラメーター i を参照していますが、プロパティ I の本体ではフィールド i を参照しています。

ベースからのメンバーによるシャドウに関する警告

コンパイラは、基底メンバーがプライマリ コンストラクター パラメーターを隠蔽していて、そのパラメーターが基底型のコンストラクターへ渡されていない場合に警告を出力します。

プライマリ コンストラクター パラメーターが class_base のコンストラクターへ渡されたとみなされるのは、次の条件がすべて満たされた場合です。

  • 引数がプライマリ コンストラクター パラメーターの暗黙的または明示的な恒等変換を表しています。
  • その引数が展開された params 引数の一部ではない。

Semantics

プライマリ コンストラクターは、指定されたパラメーターを持つ囲んでいる型のインスタンス コンストラクターを生成します。 class_base に引数リストがある場合、生成されたインスタンス コンストラクターには同じ引数リストを持つ base 初期化子があります。

クラス/構造体宣言で、プライマリ コンストラクター パラメーターには、refinout を指定できます。 レコード型のプライマリ コンストラクターでは、引き続き ref または out パラメーターを宣言することはできません。

クラス本体内のすべてのインスタンス メンバー初期化子は、生成されるコンストラクターの代入処理となります。

プライマリ コンストラクター パラメーターがインスタンス メンバー内で参照されており、その参照が nameof 引数内にない場合、そのパラメーターはコンストラクターの実行完了後もアクセス可能となるように、その外側の型の状態にキャプチャされます。 考えられる実装方法は、マングルされた名前を使用した private フィールドによるものです。 読み取り専用構造体では、キャプチャされたフィールドも読み取り専用になります。 そのため、読み取り専用構造体のキャプチャされたパラメーターへのアクセスは、読み取り専用フィールドへのアクセスと同様の制約を受けます。 読み取り専用メンバー内でのキャプチャされたパラメーターへのアクセスは、同じコンテキストでのインスタンス フィールドへのアクセスと同様の制約を受けます。

参照型のようなパラメーターのキャプチャは許可されず、refinout パラメーターのキャプチャも許可されません。 これはラムダ式でのキャプチャの制限と同様です。

プライマリ コンストラクター パラメーターがインスタンス メンバーの初期化子内でのみ参照される場合、それらの初期化子は、生成されたコンストラクターの一部として実行されるため、コンストラクターのパラメーターを直接参照できます。

プライマリ コンストラクターは以下の順序で処理を実行します。

  1. パラメーター値は、キャプチャ フィールドがあれば、そこに格納される
  2. インスタンス初期化子が実行される
  3. 基底クラスのコンストラクター初期化子が呼び出される

ユーザー コード内のパラメーター参照は、対応するキャプチャ フィールドへの参照に置き換えられます。

たとえば、次のような宣言があるとします。

public class C(bool b, int i, string s) : B(b) // b passed to base constructor
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s) : this(true, 0, s) { } // must call this(...)
}

次のようなコードが生成されます。

public class C : B
{
    public int I { get; set; }
    public string S
    {
        get => __s;
        set => __s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s) : this(0, s) { ... } // must call this(...)
    
    // generated members
    private string __s; // for capture of s
    public C(bool b, int i, string s)
    {
        __s = s; // capture s
        I = i; // run I's initializer
        B(b) // run B's constructor
    }
}

プライマリ コンストラクター以外のコンストラクターの宣言で、プライマリ コンストラクターと同一のパラメーター リストを定義すると、エラーになります。 プライマリ コンストラクター以外のすべてのコンストラクターの宣言では、最終的にプライマリ コンストラクターが呼び出されるように、this 初期化子を使用する必要があります。

レコード型では、プライマリ コンストラクター パラメーターが (生成される可能性のある) インスタンス初期化子や基底クラス初期化子内で読み取られない場合、警告が出力されます。 クラスや構造体のプライマリ コンストラクター パラメーターについても、同様の警告が報告されます。

  • 値渡しのパラメーターで、パラメーターがキャプチャされず、インスタンス初期化子や基底クラスの初期化子内で読み取られない場合。
  • in パラメーターについて、もしそのパラメーターがインスタンス初期化子または基底初期化子の中で読み取られない場合。
  • インスタンス初期化子または基本初期化子のいずれにおいてもパラメーターが読み取りまたは書き込まれていない場合、ref パラメーターについては。

同じ単純な名前と型名

通常は「Color Color」シナリオ (同じ単純な名前と型名) と呼ばれるシナリオには特別な言語規則があります。

形式 E.I のメンバー アクセスで、 E が単一の識別子であり、 Esimple_name (§12.8.4) としての意味が、 Etype_name (§7.8.1) としての意味と同じ型の定数、フィールド、プロパティ、ローカル変数、またはパラメーターである場合、 E の両方の意味が許可されます。 E.I のメンバー検索は、どちらの場合も I が必ず型 E のメンバーになるので、決してあいまいにはなりません。 つまり、この規則は、コンパイル時エラーが発生した E の静的メンバーと入れ子になった型へのアクセスを許可するだけです。

プライマリ コンストラクターに関して、この規則は、インスタンス メンバー内の識別子を型の参照として解釈すべきか、それともプライマリ コンストラクター パラメーターの参照 (この場合、そのパラメーターは外側の型の状態にキャプチャされる) として解釈すべきかに影響します。 「E.I のメンバー検索は決してあいまいにならない」とはいえ、検索の結果でメンバー グループが返される場合、メンバー アクセスを完全に解決 (バインド) しないと、そのメンバー アクセスが静的メンバーを参照するのか、インスタンス メンバーを参照するのかを判断できないケースがあります。 同時に、プライマリコンストラクターパラメーターをキャプチャすると、セマンティック分析に影響を与える形で、包囲する型のプロパティが変更されます。 たとえば、型がアンマネージになり、その結果特定の制約を満たさない可能性があります。 また、パラメーターがキャプチャされるかどうかによって、どちらの解釈が成立するかが変わるシナリオさえもあります。 次に例を示します。

struct S1(Color Color)
{
    public void Test()
    {
        Color.M1(this); // Error: ambiguity between parameter and typename
    }
}

class Color
{
    public void M1<T>(T x, int y = 0)
    {
        System.Console.WriteLine("instance");
    }
    
    public static void M1<T>(T x) where T : unmanaged
    {
        System.Console.WriteLine("static");
    }
}

受け取り側が Color を値として扱う場合、パラメーターがキャプチャされ、「S1」はマネージドになります。 その結果、制約により静的メソッドは適用できなくなり、インスタンス メソッドが呼び出されることになります。 しかし、受け取り側を型として扱う場合、パラメーターはキャプチャされず、「S1」はアンマネージドのままとなり、そのため、両方のメソッドが適用可能となりますが、静的メソッドは省略可能なパラメーターを持たないため、「より適切」と判断されます。 どちらの解釈を選択してもエラーにはなりませんが、それぞれ異なる動作結果となります。

この仕様により、次の条件がすべて満たされる場合、コンパイラは E.I のメンバー アクセスに関してあいまい性エラーを出力します。

  • E.I のメンバー検索の結果が、インスタンス メンバーと静的メンバーの両方を含むメンバー グループとなる場合。 このチェックでは、受け取り側の型に適用できる拡張メソッドはインスタンス メソッドとして扱われます。
  • E を型名ではなく単なる名前として扱う場合、それはプライマリ コンストラクター パラメーターを指し、外側の型の状態にキャプチャされることになります。

二重格納の警告

プライマリ コンストラクター パラメーターがベースに渡されると、さらに がキャプチャされる場合、オブジェクト内に誤って2回格納される可能性が高くなります。

次のすべての条件が満たされる場合、コンパイラは class_baseargument_list 内の in 引数または値渡し引数について、警告を出力します。

  • 引数がプライマリ コンストラクター パラメーターの暗黙的または明示的な恒等変換を表しています。
  • その引数が展開された params 引数の一部ではない。
  • プライマリ コンストラクタ パラメーターは、囲む型の状態に記録されます。

次のすべての条件が満たされる場合、コンパイラは variable_initializer について警告を出力します。

  • 変数初期化子がプライマリ コンストラクター パラメーターの暗黙的または明示的な恒等変換を表している場合。
  • プライマリ コンストラクタ パラメーターは、囲む型の状態に記録されます。

次に例を示します。

public class Person(string name)
{
    public string Name { get; set; } = name;   // warning: initialization
    public override string ToString() => name; // capture
}

プライマリ コンストラクターを対象とする属性

https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-03-13.md で、https://github.com/dotnet/csharplang/issues/7047 の提案を採用することを決定しました。

"method" 属性のターゲットは、class_declaration/struct_declarationparameter_list において許可され、対応するプライマリコンストラクターがその属性を有することになります。 method のない class_declaration/struct_declaration 上の ターゲットを持つ属性は無視され、警告が表示されます。

[method: FooAttr] // Good
public partial record Rec(
    [property: Foo] int X,
    [field: NonSerialized] int Y
);
[method: BarAttr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public partial record Rec
{
    public void Frobnicate()
    {
        ...
    }
}
[method: Attr] // Good
public record MyUnit1();
[method: Attr] // warning CS0657: 'method' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'type'. All attributes in this block will be ignored.
public record MyUnit2;

レコードの主要コンストラクター

この提案により、レコード型では独自のプライマリ コンストラクターの仕組みを個別に規定する必要がなくなります。 代わりに、プライマリ コンストラクターを持つレコード型 (クラスおよび構造体) の宣言は、次の追加規則を除き、一般的な規則に従います。

  • プライマリ コンストラクターの各パラメーターについて、同名のメンバーが既に存在する場合、そのメンバーはインスタンス プロパティまたはフィールドである必要があります。 そうでない場合は、同じ名前のパブリック init 専用の自動プロパティが、パラメーターから割り当てられたプロパティ初期化子と合成されます。
  • プライマリ コンストラクター パラメーターに対応する out パラメーターを持つデコンストラクターが生成されます。
  • 明示的なコンストラクター宣言が「コピー コンストラクター」 (外側の型の 1 つのパラメーターを取るコンストラクター) である場合、this 初期化子の呼び出しは不要であり、レコード型の宣言内に存在するメンバー初期化子は実行されません。

デメリット

  • 生成されるオブジェクトのメモリ割り当てサイズは直感的にはわかりにくくなります。これは、コンパイラがクラスのコード全体を解析し、プライマリ コンストラクターの各パラメーターについてフィールドを割り当てるかどうかを判断するためです。 このリスクは、ラムダ式による変数の暗黙的なキャプチャに似ています。
  • 継承チェーンにおいて、基底クラスに明示的に protected フィールドを割り当てるべき場面で、コンストラクター チェーンを通じて「同じ」パラメーターを繰り返し渡してしまい、結果として各派生クラスごとにデータが重複してキャプチャされてしまうことがよくあります。 これは、自動プロパティを自動プロパティでオーバーライドする現在のリスクによく似ています。
  • ここで提案されているように、通常はコンストラクター本体で用いる可能性のある追加のロジックを含める余地はありません。 この問題は、次の「プライマリ コンストラクター本体」の拡張によって解決されます。
  • 提案されている仕様では、通常のコンストラクターとは微妙に異なる実行順序のセマンティクスとなり、メンバー初期化子の実行が基底クラスの呼び出しの後まで遅延されます。 これはおそらく改善できるかもしれませんが、いくつかの拡張機能提案(特に「プライマリ コンストラクター本体」)が犠牲になる可能性があります。
  • この提案は、1 つのコンストラクターをプライマリとして指定できるシナリオにのみ適用されます。
  • クラスとプライマリ コンストラクターで異なるアクセシビリティを表現する方法はありません。 たとえば、すべての public コンストラクターが private な「全機能」コンストラクターに委譲する場合などです。 必要であれば、後でそのための構文を提案することもできます。

代替

キャプチャなし

この機能の簡易版として、メンバー本体内でのプライマリ コンストラクター パラメーターの使用を禁止することが考えられます。 それを参照することは間違いです。 初期化コード以降も格納データが必要な場合は、フィールドを明示的に宣言する必要があります。

public class C(string s)
{
    public string S1 => s; // Nope!
    public string S2 { get; } = s; // Still allowed
}

これは後で完全な提案に発展させることができ、多くの設計上の意思決定や複雑さを回避できますが、初期段階では定型コードをあまり削除できず、直感的でないと感じられる可能性があります。

明示的に生成されるフィールド

別のアプローチとして、プライマリ コンストラクター パラメーターごとに、対応するフィールドを常に明示的に生成する方法があります。 プライマリ コンストラクター パラメーターは、ローカル関数や無名関数のようにキャプチャされるのではなく、レコード型のプライマリ コンストラクター パラメーターの public プロパティと同様に、明示的なメンバーとして宣言されます。 レコード型と同様に、適切なメンバーが既に存在する場合、メンバーは生成されません。

生成されるフィールドが private の場合、メンバー本体で使用されなければ省略可能です。 クラスでは、しかし、派生クラスで状態の重複が生じる可能性があるため、多くの場合 private フィールドは適切な選択とはなりません。 代替案として、クラスでは protected フィールドを生成することで、継承階層全体で格納データの再利用を促すという方法が考えられます。 ただし、その場合、宣言を省略できなくなり、すべてのプライマリ コンストラクター パラメーターに割り当てコストが発生します。

これにより、レコード以外のプライマリコンストラクターは、レコードのプライマリコンストラクターにさらに近づくことになります。その理由は、異なる種類のメンバーが異なるアクセス権を持ちながらも、常に(少なくとも概念的には)生成されるためです。 しかし、この方法では、C# の他の箇所でのパラメーターやローカル変数のキャプチャ方法と異なる、予期しない動作が生じることになります。 たとえば、将来的にローカル クラスを許可することになった場合、それらは外側のパラメーターやローカル変数を暗黙的にキャプチャすることになります。 その対策として、隠蔽フィールドを生成することは、妥当な動作とは言えないでしょう。

このアプローチに関してよく指摘される別の問題は、多くの開発者がパラメーターとフィールドで異なる命名規則を使用しているという点です。 プライマリ コンストラクター パラメーターにはどちらの命名規則を使用すべきでしょうか。 いずれを選択しても、コードの他の部分との一貫性が失われることになります。

最後に、メンバー宣言を明確に生成することは、実際にはレコードにおいては普通のことですが、レコード以外のクラスや構造体の場合は、はるかに驚くべき特異なことになります。 全体として、それらは主な提案が暗黙的なキャプチャを選択する理由であり、必要なときに明示的なメンバー宣言に対する賢明な動作 (レコードと一致) があります。

初期化子のスコープからインスタンス メンバーを除外する

前述の検索規則は、対応するメンバーが手動で宣言されている場合のレコード型におけるプライマリ コンストラクター パラメーターの現在の動作を許可し、メンバーが生成されない場合の動作を説明することを意図しています。 これには、「初期化スコープ」 (this/base 初期化子、メンバー初期化子) と「本体スコープ」 (メンバー本体) の間で異なるルックアップが必要です。上記の提案では、参照がどこで発生するかに応じて、プライマリ コンストラクター パラメーターが検索されるタイミングを変更することでこれを実現しています。

注目すべき点は、初期化子のスコープ内で単純名でインスタンス メンバーを参照すると、常にエラーが発生することです。 それらの場所でインスタンス メンバーを単に隠蔽するのではなく、完全にスコープ外にすることはできないでしょうか。 そうすれば、スコープの順序に関する複雑な条件が不要となり、仕様が単純化されます。

この代替案は可能かもしれませんが、影響範囲が広がり、望ましくない結果を引き起こす可能性もあります。 まず、初期化子スコープからインスタンス メンバーを削除すると、インスタンス メンバーに対応し、プライマリ コンストラクター パラメーターに対応しない単純な名前が、誤って型宣言外部の何かにバインドされる可能性があります。 ほとんどの場合、意図的に発生する状況ではないため、エラーとして扱うほうが適切でしょう。

さらに、静的 メンバーは初期化スコープで参照しても問題ありません。 そのため、この変更を適用するには、静的メンバーとインスタンス メンバーを検索時に区別する必要がありますが、この言語で現在そのような処理を行っていません。 (オーバーロードの解決においては区別しますが、それはここでは関係ありません)。 したがって、これも変更する必要があり、さらに多くの状況が発生します。静的コンテキストでは、インスタンス メンバーが見つかったため、エラーではなく何かが「さらに外側」にバインドされます。

最終的に、この「単純化」は、だれも求めていない下流での不要な複雑さにつながるでしょう。

拡張の可能性

これらの変更案は、現在の提案に加えて検討されるか、後の段階で有用と判断された場合に適用される可能性があります。

コンストラクター内でのプライマリパラメーターへのアクセス

上記の規則では、プライマリ コンストラクター パラメーターを別のコンストラクター内で参照するとエラーになります。 ただし、プライマリ コンストラクターが最初に実行されるため、他のコンストラクターの本体内ではこれが許可される可能性があります。 しかし、this 初期化子の引数リスト内での参照は引き続き禁止する必要があります。

public class C(bool b, int i, string s) : B(b)
{
    public C(string s) : this(b, s) // b still disallowed
    { 
        i++; // could be allowed
    }
}

そのようなアクセスは、プライマリ コンストラクターの実行後にコンストラクター本体が変数にアクセスする唯一の方法となるため、依然としてキャプチャが発生します。

プライマリ コンストラクター パラメーターを this 初期化子の引数リスト内で使用可能にしつつ、確実に代入されることを保証しない方法も考えられますが、それは実用的とは言えません。

this 初期化子なしのコンストラクターを許可する

this 初期化子を持たないコンストラクター (つまり、暗黙的または明示的な base 初期化子を持つコンストラクター) を許可することも考えられます。 このようなコンストラクターは、インスタンス フィールド、プロパティ、およびイベント初期化子を実行しません。これらはプライマリ コンストラクターの一部のみとみなされるためです。

このような base 呼び出しを行うコンストラクターが存在する場合、プライマリ コンストラクター パラメーターのキャプチャの扱いにはいくつかの選択肢があります。 最も単純な方法は、この状況でのキャプチャを完全に禁止することです。 このようなコンストラクターが存在する場合、プライマリ コンストラクター パラメーターは初期化専用となります。

前に説明したオプションと組み合わせて、コンストラクターでプライマリコンストラクターパラメーターへのアクセスを許可すると、パラメーターは確定的に割り当てられていない状態でコンストラクター本体に入ることがあります。キャプチャされたパラメーターは、コンストラクター本体の終了時までに確実に割り当てられる必要があります。 実質的には暗黙的な out パラメーターとして機能することになります。 このアプローチにより、キャプチャされたプライマリ コンストラクター パラメーターは、他の関数メンバーで使用される時点で、必ず意味のある値 (つまり明示的に代入された値) を持つことになります。

この拡張 (いずれの形式でも) の利点は、レコード型の「コピー コンストラクター」に対する現在の例外規則を完全に一般化し、未初期化のプライマリ コンストラクター パラメーターが参照される状況を防ぐことにあります。 つまり、オブジェクトの初期化方法が異なっていても、問題なく動作することになります。 キャプチャに関する制限は、レコード型における手動で定義されたコピー コンストラクターに対して互換性のない変更とはなりません。なぜなら、レコード型はプライマリ コンストラクター パラメーターをキャプチャせず、代わりにフィールドを生成するからです。

public class C(bool b, int i, string s) : B(b)
{
    public int I { get; set; } = i; // i used for initialization
    public string S // s used directly in function members
    {
        get => s;
        set => s = value ?? throw new ArgumentNullException(nameof(value));
    }
    public C(string s2) : base(true) // cannot use `string s` because it would shadow
    { 
        s = s2; // must initialize s because it is captured by S
    }
    protected C(C original) : base(original) // copy constructor
    {
        this.s = original.s; // assignment to b and i not required because not captured
    }
}

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

コンストラクター自体には、初期化子として表現できないパラメーターの検証ロジックやその他の複雑な初期化コードが含まれることがよくあります。

そのため、プライマリ コンストラクターを拡張して、クラス本体内に直接ステートメント ブロックを記述できるようにすることも可能です。 これらのステートメントは、生成されたコンストラクター内で、初期化処理の合間に挿入され、順次実行されることになります。 例:

public class C(int i, string s) : B(s)
{
    {
        if (i < 0) throw new ArgumentOutOfRangeException(nameof(i));
    }
	int[] a = new int[i];
    public int S => s;
}

"コンストラクター および の後にオブジェクト/コレクション初期化子が完了した際に実行される「最終イニシャライザー」を導入することで、このシナリオの多くが適切にカバーされる可能性があります。" ただし、引数の検証はできる限り早い段階で行うことが理想的です。

プライマリ コンストラクター本体は、プライマリ コンストラクターにアクセス修飾子を設定する場所として機能し、これにより、それが囲んでいる型のアクセスレベルとは異なる設定を可能にすることもできます。

パラメーターとメンバーの宣言の組み合わせ

可能で、よく言及される追加点として、プライマリコンストラクターパラメーターに注釈を付けることで、その型にメンバーを宣言できるようにすることです。 最も一般的な提案として、パラメーターにアクセス修飾子を指定することで、自動的にメンバーを生成する方法があります。

public class C(bool b, protected int i, string s) : B(b) // i is a field as well as a parameter
{
    void M()
    {
        ... i ... // refers to the field i
        ... s ... // closes over the parameter s
    }
}

しかし、いくつかの問題があります。

  • フィールドではなくプロパティが必要な場合はどうするか。 パラメーター リスト内に { get; set; } のような構文を埋め込むのは直感的とは言えません。
  • パラメーターとフィールドで異なる命名規則が使用されている場合はどうするか。 その場合、この機能は役に立ちません。

この機能は、現時点での追加は見送りますが、 将来の採用の可能性は排除せずに残しておきます。

未解決の質問

型パラメーターの検索順序

https://github.com/dotnet/csharplang/blob/main/proposals/csharp-12.0/primary-constructors.md#lookup セクションでは、パラメーターがスコープ内にあるすべてのコンテキストにおいて、宣言型の型パラメーターはその型のプライマリ コンストラクター パラメーターよりも前に検索される必要があると規定されています。 しかし、レコード型では、基底クラスの初期化子やフィールド初期化子の中で、プライマリ コンストラクター パラメーターが型パラメーターよりも優先されています。

この不一致についてどうすればよいでしょうか?

  • 動作に合わせて規則を調整する。
  • 動作を調整します (重大な変更の可能性があります)。
  • プライマリ コンストラクター パラメーターが型パラメーターの名前を使用することを禁止します (重大な変更の可能性があります)。
  • 何もせず、仕様と実装の不一致を受け入れる。

結論:

動作に合わせて規則を調整する (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-09-25.md#primary-constructors)。

キャプチャされたプライマリ コンストラクター パラメーターに対するフィールド用の属性

キャプチャされたプライマリ コンストラクター パラメーターに対してフィールド用の属性を許可すべきでしょうか?

class C1([field: Test] int x) // Parameter is captured, the attribute goes to the capture field
{
    public int X => x;
}
class C2([field: Test] int x) // Parameter is not captured, the attribute is ignored with a warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
    public int X = x;
}

現在、パラメーターがキャプチャされているかどうかに関係なく、属性は警告とともに無視されます。

レコードにおいては、フィールドに対するターゲット属性はプロパティが合成される際に許可されることに注意してください。 その後、属性はバッキング フィールドに移動します。

record R1([field: Test]int X); // Ok, the attribute goes on the backing field
record R2([field: Test]int X) // warning CS0657: 'field' is not a valid attribute location for this declaration. Valid attribute locations for this declaration are 'param'. All attributes in this block will be ignored.
{
    public int X = X;
}

結論:

許可されない (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-03.md#attributes-on-captured-parameters)。

ベースからのメンバーによるシャドウに関する警告

メンバー内で基底クラスのメンバーがプライマリ コンストラクター パラメーターを隠蔽している場合に警告を報告すべきでしょうか (https://github.com/dotnet/csharplang/discussions/7109#discussioncomment-5666621 を参照) ?

結論:

代替設計が承認済み - https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-08.md#primary-constructors

クロージャ内で囲む型のインスタンスのキャプチャ

囲む型の状態に取り込まれたパラメーターが、インスタンス初期化子または基本初期化子内のラムダでも参照されている場合、ラムダと囲む型の状態はパラメーターの同じ場所を参照する必要があります。 次に例を示します。

partial class C1
{
    public System.Func<int> F1 = Execute1(() => p1++);
}

partial class C1 (int p1)
{
    public int M1() { return p1++; }
    static System.Func<int> Execute1(System.Func<int> f)
    {
        _ = f();
        return f;
    }
}

パラメーターを型の状態にキャプチャする単純な実装では、パラメーターは単に private インスタンス フィールドにキャプチャされるため、ラムダ式はこの同じフィールドを参照する必要があります。 したがって、ラムダ式は型のインスタンスにアクセスできる必要があります。 そのためには、基底クラスのコンストラクターが呼び出される前に this をクロージャにキャプチャする必要があります。 その結果、安全ではあるものの、検証不可能な IL が生成されることになります。 これは許容できるのでしょうか?

代替案として以下が考えられます。

  • そのようなラムダを禁止します。
  • または、そのようなパラメーターを別のクラスのインスタンス (別のクロージャ) にキャプチャし、そのインスタンスをクロージャと外側の型のインスタンス間で共有する方法も考えられます。 これにより、クロージャ内で this をキャプチャする必要がなくなります。

結論:

基本コンストラクターが呼び出される前に、this をクロージャにキャプチャすることに慣れています (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md)。 ランタイム チームもこの IL パターンに問題があるとは考えていません。

構造体内の this への割り当て

C# では、構造体内で this への代入が許可されています。 構造体がプライマリ コンストラクター パラメーターをキャプチャする場合、代入によってその値が上書きされますが、これはユーザーにとってわかりにくい可能性があります。 このような割り当てに対して警告を報告したいと思いますか?

struct S(int x)
{
    int X => x;
    
    void M(S s)
    {
        this = s; // 'x' is overwritten
    }
}

結論:

許可、警告なし (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-02-15.md)。

初期化とキャプチャの両方による二重格納の警告

プライマリ コンストラクター パラメーターが基底クラスに渡され、かつキャプチャされる場合、オブジェクト内に意図せず二重に格納されるリスクが高いため、警告が出力されます。

パラメーターがメンバーの初期化に使用され、かつキャプチャされる場合にも同様のリスクがあるようです。 簡単な例を次に示します。

public class Person(string name)
{
    public string Name { get; set; } = name;   // initialization
    public override string ToString() => name; // capture
}

この Person のインスタンスでは、Name の変更が ToString の出力に反映されず、これはおそらく開発者の意図しない動作です。

この状況についても二重格納の警告を導入すべきでしょうか?

その動作を以下に示します。

次のすべての条件が満たされる場合、コンパイラは variable_initializer について警告を出力します。

  • 変数初期化子がプライマリ コンストラクター パラメーターの暗黙的または明示的な恒等変換を表している場合。
  • プライマリ コンストラクタ パラメーターは、囲む型の状態に記録されます。

結論:

承認済み (https://github.com/dotnet/csharplang/blob/main/meetings/2023/LDM-2023-05-15.md#primary-constructors を参照)。

LDM ミーティング