必須メンバー
手記
この記事は機能仕様です。 仕様は、機能の設計ドキュメントとして機能します。 これには、提案された仕様の変更と、機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が最終決定され、現在の ECMA 仕様に組み込まれるまで公開されます。
機能の仕様と完成した実装の間には、いくつかの違いがある可能性があります。 これらの違いは、関連する 言語設計会議 (LDM) ノートでキャプチャされます。
機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。
概要
この提案では、オブジェクトの初期化時にプロパティまたはフィールドを設定する必要があることを指定する方法が追加され、インスタンス作成者は作成サイトでオブジェクト初期化子のメンバーの初期値を指定する必要があります。
モチベーション
現在、オブジェクト階層では、階層のすべてのレベルにわたってデータを運ぶのに多くの定型句が必要です。 C# 8 で定義されている Person
を含む単純な階層を見てみましょう。
class Person
{
public string FirstName { get; }
public string MiddleName { get; }
public string LastName { get; }
public Person(string firstName, string lastName, string? middleName = null)
{
FirstName = firstName;
LastName = lastName;
MiddleName = middleName ?? string.Empty;
}
}
class Student : Person
{
public int ID { get; }
public Student(int id, string firstName, string lastName, string? middleName = null)
: base(firstName, lastName, middleName)
{
ID = id;
}
}
ここで多くの繰り返しが行われます。
- 階層のルートでは、各プロパティの型を 2 回繰り返す必要があり、名前を 4 回繰り返す必要がありました。
- 派生レベルでは、継承された各プロパティの型を 1 回繰り返す必要があり、名前を 2 回繰り返す必要がありました。
これは、3 つのプロパティと 1 レベルの継承を持つ単純な階層ですが、これらの種類の階層の多くの実際の例では、多くのレベルが深くなり、より大きく、より多くの数のプロパティが蓄積されます。 Roslyn は、たとえば、CST と AST を作成するさまざまなツリー型のコードベースの 1 つです。 この入れ子は、これらの型のコンストラクターと定義を生成するコード ジェネレーターがあるほど面倒であり、多くのお客様が同様の方法で問題に取り組んでいます。 C# 9 ではレコードが導入されています。一部のシナリオでは、これを改善できます。
record Person(string FirstName, string LastName, string MiddleName = "");
record Student(int ID, string FirstName, string LastName, string MiddleName = "") : Person(FirstName, LastName, MiddleName);
record
重複の最初のソースは排除されますが、2 つ目の重複元は変更されません。残念ながら、これは階層の成長に合わせて増加する重複の原因であり、階層を変更した後に修正する最も痛みを伴う部分です。階層をすべての場所で追跡する必要があるため、 場合によっては、プロジェクトをまたいで、潜在的に消費者を壊す可能性があります。
この重複を回避する方法として、コンストラクターの記述を避けるためにオブジェクト初期化子を採用する消費者が長年にわたって増えてきました。 ただし、C# 9 より前は、次の 2 つの大きな欠点がありました。
- オブジェクト階層は、すべてのプロパティに
set
アクセサーを使用して、完全に変更可能である必要があります。 - グラフからオブジェクトをインスタンス化するたびにすべてのメンバーが設定されるようにする方法はありません。
C# 9 では、init
アクセサーを導入することで、ここでも最初の問題に対処しました。これを使用すると、これらのプロパティはオブジェクトの作成/初期化時に設定できますが、その後は設定できません。 ただし、もう一度 2 つ目の問題があります。C# のプロパティは、C# 1.0 以降は省略可能です。 C# 8.0 で導入された Null 許容参照型は、この問題の一部に対処しました。コンストラクターが null 非許容参照型プロパティを初期化しない場合、ユーザーに警告が表示されます。 ただし、これは問題を解決しません。ここでのユーザーは、コンストラクターで型の大部分を繰り返したくない、要件 を渡して、コンシューマーにプロパティを設定したいと考えています。 値型であるため、Student
からの ID
に関する警告も表示されません。 これらのシナリオは、EF Core などのデータベース モデルの ORM で非常に一般的です。EF Core では、パブリック パラメーターなしのコンストラクターを使用する必要がありますが、プロパティの null 許容に基づいて行の null 許容を推進する必要があります。
この提案では、必要なメンバーである C# に新機能を導入することで、これらの問題に対処することを目指しています。 必要なメンバーは、複数のコンストラクターやその他のシナリオに柔軟に対応できるようにさまざまなカスタマイズを行い、型作成者ではなくコンシューマーによって初期化される必要があります。
詳細な設計
class
、struct
、および record
型を使用すると、required_member_listを宣言できます。 この一覧は、必須と見なされる型であり、型のインスタンスの構築と初期化の間に初期化する必要がある、すべてのプロパティとフィールドの一覧です。 型は、基本型からこれらのリストを自動的に継承し、定型コードと繰り返しコードを削除するシームレスなエクスペリエンスを提供します。
required
修飾子
field_modifier と property_modifierの修飾子の一覧に 'required'
を追加します。 型の required_member_list は、required
が適用されたすべてのメンバーで構成されています。 したがって、以前の Person
型は次のようになります。
public class Person
{
// The default constructor requires that FirstName and LastName be set at construction time
public required string FirstName { get; init; }
public string MiddleName { get; init; } = "";
public required string LastName { get; init; }
}
required_member_list を持つ型のすべてのコンストラクターは、その型の利用者がリスト内のすべてのプロパティを初期化しなければならないという コントラクト を自動的に示します。 少なくともコンストラクター自体と同じようにアクセス可能ではないメンバーを必要とするコントラクトを宣言するのは、コンストラクターのエラーです。 例えば:
public class C
{
public required int Prop { get; protected init; }
// Advertises that Prop is required. This is fine, because the constructor is just as accessible as the property initer.
protected C() {}
// Error: ctor C(object) is more accessible than required property Prop.init.
public C(object otherArg) {}
}
required
は、class
、struct
、および record
型でのみ有効です。 interface
型では無効です。 required
を次の修飾子と組み合わせることはできません。
fixed
ref readonly
ref
const
static
required
インデクサーへの適用は許可されていません。
コンパイラは、Obsolete
が型の必要なメンバーに適用されると警告を発行します。
- 型が
Obsolete
としてマークされていない、または SetsRequiredMembersAttribute
に属性が設定されていないコンストラクターは、Obsolete
マークされません。
SetsRequiredMembersAttribute
必要なメンバーを持つ型、または基本型で必要なメンバーを指定するすべてのコンストラクターは、そのコンストラクターが呼び出されたときにコンシューマーによってそれらのメンバーが設定されている必要があります。 この要件からコンストラクターを除外するために、コンストラクターを SetsRequiredMembersAttribute
に属性付けして、これらの要件を削除できます。 コンストラクターの本体は、それによって型の必須メンバーが確実に設定されることは確認されていません。
SetsRequiredMembersAttribute
でマークし、エラーは報告されません。
コンストラクター C
が SetsRequiredMembersAttribute
で属性付けされた base
または this
コンストラクターにチェーンする場合は、C
も必ず SetsRequiredMembersAttribute
で属性付けされている必要があります。
レコード型に関して、レコード型またはその基底型に必須メンバーが含まれている場合は、合成されたコピーコンストラクターに SetsRequiredMembersAttribute
を出力します。
NB:この提案の以前のバージョンでは、初期化に関してより大きなメタ言語があり、コンストラクターから個々の必須メンバーを追加および削除したり、コンストラクターが必要なすべてのメンバーを設定していることを検証したりできます。 これは最初のリリースでは複雑すぎると見なされ、削除されました。 後の機能として、より複雑なコントラクトと変更を追加する方法について説明します。
適用
必要なメンバーが R
T
型のすべてのコンストラクター Ci
について、Ci
を呼び出すコンシューマーは、次のいずれかの操作を行う必要があります。
- object_initializer 内の
R
のすべてのメンバーを、object_creation_expression で設定します。 - または、attribute_targetの named_argument_list セクションを使用して、
R
のすべてのメンバーを設定します。
Ci
が SetsRequiredMembers
に起因する場合を除く。
現在のコンテキストで object_initializer が許可されていないか、attribute_targetでない場合、Ci
が SetsRequiredMembers
に属性付けされていない場合は、Ci
を呼び出すエラーになります。
new()
制約
コントラクト をアドバタイズするパラメーターなしのコンストラクターを持つ型は、new()
に制約された型パラメーターに置き換えることはできません。これは、ジェネリックインスタンス化で要件が満たされるようにする方法がないためです。
struct
default
s
必須メンバーは、default
または default(StructType)
で作成された struct
型のインスタンスには適用されません。 StructType
にパラメーターなしのコンストラクターがなく、既定の構造体コンストラクターが使用されている場合でも、new StructType()
で作成された struct
インスタンスに適用されます。
アクセシビリティ
包含型が表示されているコンテキストでメンバーを設定できない場合は、メンバーを必須としてマークするとエラーになります。
- メンバーがフィールドの場合は、
readonly
であってはなりません。 - メンバーがプロパティの場合は、少なくともメンバーの包含型と同じくらいアクセス可能なセッターまたは初期化子が必要です。
これは、次の場合は許可されないことを意味します。
interface I
{
int Prop1 { get; }
}
public class Base
{
public virtual int Prop2 { get; set; }
protected required int _field; // Error: _field is not at least as visible as Base. Open question below about the protected constructor scenario
public required readonly int _field2; // Error: required fields cannot be readonly
protected Base() { }
protected class Inner
{
protected required int PropInner { get; set; } // Error: PropInner cannot be set inside Base or Derived
}
}
public class Derived : Base, I
{
required int I.Prop1 { get; } // Error: explicit interface implementions cannot be required as they cannot be set in an object initializer
public required override int Prop2 { get; set; } // Error: this property is hidden by Derived.Prop2 and cannot be set in an object initializer
public new int Prop2 { get; }
public required int Prop3 { get; } // Error: Required member must have a setter or initer
public required int Prop4 { get; internal set; } // Error: Required member setter must be at least as visible as the constructor of Derived
}
required
メンバーはコンシューマーによって設定されなくなったため、非表示にすることはエラーです。
required
メンバーをオーバーライドする場合は、メソッド シグネチャに required
キーワードを含める必要があります。 これは、将来オーバーライドを使ってプロパティを必須でなくすることを許可したい場合に、そのための設計の余地を確保しておくために行われます。
オーバーライドを使用して、基本型でrequired
ではなかったメンバーrequired
をマークできます。 派生型の必須メンバー リストに、そのようにマークされたメンバーが追加されます。
型は、必要な仮想プロパティをオーバーライドできます。 つまり、基本仮想プロパティにストレージがあり、派生型がそのプロパティの基本実装にアクセスしようとすると、初期化されていないストレージが観察される可能性があります。 NB:これは一般的なC#アンチパターンであり、この提案はそれに対処しようとするべきではないと思います。
null 許容分析への影響
required
マークされているメンバーは、コンストラクターの末尾で有効な null 許容状態に初期化する必要はありません。 この型および基本型のすべての required
メンバーは、null 許容分析によって、その型のコンストラクターの先頭で既定値と見なされます。ただし、SetsRequiredMembersAttribute
属性が設定された this
または base
のコンストラクターと連結されている場合を除きます。
null 許容分析では、現在の型および基本型の required
メンバーのうち、SetsRequiredMembersAttribute
属性が設定されているコンストラクターの末尾で有効な null 許容状態になっていないすべてのメンバーについて警告が発行されます。
#nullable enable
public class Base
{
public required string Prop1 { get; set; }
public Base() {}
[SetsRequiredMembers]
public Base(int unused) { Prop1 = ""; }
}
public class Derived : Base
{
public required string Prop2 { get; set; }
[SetsRequiredMembers]
public Derived() : base()
{
} // Warning: Prop1 and Prop2 are possibly null.
[SetsRequiredMembers]
public Derived(int unused) : base()
{
Prop1.ToString(); // Warning: possibly null dereference
Prop2.ToString(); // Warning: possibly null dereference
}
[SetsRequiredMembers]
public Derived(int unused, int unused2) : this()
{
Prop1.ToString(); // Ok
Prop2.ToString(); // Ok
}
[SetsRequiredMembers]
public Derived(int unused1, int unused2, int unused3) : base(unused1)
{
Prop1.ToString(); // Ok
Prop2.ToString(); // Warning: possibly null dereference
}
}
メタデータ表現
次の 2 つの属性は C# コンパイラで認識されており、この機能を機能させるために必要です。
namespace System.Runtime.CompilerServices
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class RequiredMemberAttribute : Attribute
{
public RequiredMemberAttribute() {}
}
}
namespace System.Diagnostics.CodeAnalysis
{
[AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)]
public sealed class SetsRequiredMembersAttribute : Attribute
{
public SetsRequiredMembersAttribute() {}
}
}
型に RequiredMemberAttribute
を手動で適用するとエラーになります。
required
マークされているメンバーには、RequiredMemberAttribute
が適用されます。 さらに、このようなメンバーを定義するすべての型は、この型に必要なメンバーがあることを示すマーカーとして、RequiredMemberAttribute
でマークされます。 型 B
が A
から派生し、A
が required
メンバーを定義しているが、B
新しいメンバーを追加したり、既存の required
メンバーをオーバーライドしたりしない場合、B
は RequiredMemberAttribute
でマークされないことに注意してください。
B
に必要なメンバーがあるかどうかを完全に判断するには、完全な継承階層を確認する必要があります。
SetsRequiredMembersAttribute
が適用されていない required
メンバーを持つ型のコンストラクターには、次の 2 つの属性が付けられます。
-
System.Runtime.CompilerServices.CompilerFeatureRequiredAttribute
と機能名"RequiredMembers"
。 System.ObsoleteAttribute
を文字列"Types with required members are not supported in this version of your compiler"
と共に使用し、これらのコンストラクターを古いコンパイラで使用しないようにするために、属性がエラーとしてマークされます。
ここでは、バイナリ互換性を維持することが目標であるため、modreq
を使用しません。最後の required
プロパティが型から削除された場合、コンパイラはこの modreq
を合成しなくなります。これはバイナリ破壊的変更であり、すべてのコンシューマーを再コンパイルする必要があります。 required
メンバーを認識するコンパイラは、この古い属性を無視します。 メンバーは基本型からも取得できることに注意してください。現在の型に新しい required
メンバーがない場合でも、基本型にメンバー required
がある場合は、この Obsolete
属性が生成されます。 コンストラクターに既に Obsolete
属性がある場合、追加の Obsolete
属性は生成されません。
ObsoleteAttribute
と CompilerFeatureRequiredAttribute
の両方を使用します。後者は今回のリリースの新機能であり、古いコンパイラではそれを理解していないためです。 将来的には、ObsoleteAttribute
を削除したり、新しい機能を保護するために使用したりできない可能性がありますが、現時点では、完全な保護のために両方が必要です。
特定の型 T
に対して R
required
メンバーの完全な一覧 (すべての基本型を含む) を作成するには、次のアルゴリズムが実行されます。
- すべての
Tb
について、T
から始めてobject
に達するまで、基本型チェーンをたどります。 Tb
がRequiredMemberAttribute
でマークされている場合、RequiredMemberAttribute
でマークされたTb
のすべてのメンバーがRb
に収集されます。Rb
内のすべてのRi
について、Ri
がR
のメンバーによってオーバーライドされた場合、スキップされます。- それ以外の場合、
Ri
がR
のメンバーによって非表示になっている場合、必要なメンバーの参照は失敗し、それ以上の手順は実行されません。SetsRequiredMembers
に属性付けされていないT
のコンストラクターを呼び出すと、エラーが発生します。 - それ以外の場合は、
Ri
がR
に追加されます。
質問を開く
入れ子になったメンバー初期化子
入れ子になったメンバー初期化子の適用機構はどのようになりますか? 完全に禁止されますか?
class Range
{
public required Location Start { get; init; }
public required Location End { get; init; }
}
class Location
{
public required int Column { get; init; }
public required int Line { get; init; }
}
_ = new Range { Start = { Column = 0, Line = 0 }, End = { Column = 1, Line = 0 } } // Would this be allowed if Location is a struct type?
_ = new Range { Start = new Location { Column = 0, Line = 0 }, End = new Location { Column = 1, Line = 0 } } // Or would this form be necessary instead?
議論された質問
init
条項の執行レベル
init
句機能は C# 11 では実装されませんでした。 これはアクティブな提案のままです。
init
句で初期化子なしに指定されたメンバーが、すべてのメンバーを初期化しなければならないことを私たちは厳格に強制しますか? 私たちがそうする可能性が高いようです。そうしないと、簡単な失敗の落とし穴が作られてしまいます。 ただし、C# 9 で MemberNotNull
で解決したのと同じ問題を再導入するリスクもあります。 これを厳密に適用する場合は、ヘルパー メソッドがメンバーを設定することを示す方法が必要になる可能性があります。 これについて説明したいくつかの構文を次に示します。
init
メソッドを許可します。 これらのメソッドは、コンストラクターまたは別のinit
メソッドからのみ呼び出すことができます。コンストラクター内にあるかのようにthis
にアクセスできます (つまり、readonly
とinit
フィールド/プロパティを設定します)。 これは、そのようなメソッドのinit
句と組み合わせることができます。 句のメンバーがメソッド/コンストラクターの本体で確実に割り当てられている場合、init
句は満たされていると見なされます。 メンバーを含むinit
句を伴うメソッドの呼び出しは、そのメンバーへの割り当てと見なされます。 これが現在または将来追求したいルートであると判断した場合は、コンストラクターの init 句のキーワードとしてinit
を使用すべきではない可能性が高いと考えられます。混乱を招く可能性があります。!
演算子が警告/エラーを明示的に抑制できるようにします。 複雑な方法 (共有メソッドなど) でメンバーを初期化する場合、ユーザーは init 句に!
を追加して、コンパイラが初期化を確認しないことを示すことができます。
結論: 議論の後、私たちは !
演算子のアイデアが好きです。 これにより、ユーザーはより複雑なシナリオについて意図的に行うことができますが、init メソッドの周りに大きなデザイン ホールを作成せず、すべてのメソッドにメンバー X または Y の設定として注釈を付けることもできます。!
は、null 許容の警告を抑制するために既に使用しているために選択されました。また、別の場所でコンパイラに "I'm smarter than you" と伝えるために使用することは、構文フォームの自然な拡張です。
必要なインターフェイス メンバー
この提案では、インターフェイスでメンバーを必須としてマークすることはできません。 これにより、現在、ジェネリックにおけるnew()
やインターフェイスの制約に関連する複雑なシナリオを理解する必要がなくなり、これはファクトリおよびジェネリックな構築に直接関係しています。 この領域に設計空間を確保するために、インターフェイスの required
を禁止し、required_member_lists を持つ型が new()
に制約された型パラメーターに置き換えられるのを禁止しました。 工場で一般的な建設シナリオを詳しく見たい場合は、この問題を再検討できます。
構文に関する質問
init
句機能は C# 11 では実装されませんでした。 これはアクティブな提案のままです。
init
正しい単語ですか? 構造体の後置修飾子としてinit
を使うと、その後ファクトリ向けに再利用しようとする際に干渉する可能性があり、さらにプレフィックス修飾子を持つinit
メソッドを有効化することもできます。 その他の可能性:set
required
、すべてのメンバーが初期化されることを指定するための適切な修飾子ですか? その他の提案:default
all
- ! を使用する 複雑なロジックを示す場合
base
/this
とinit
の間に区切り記号が必要ですか?-
:
区切り記号 - ',' 区切り記号
-
required
は適切な修飾子ですか? 推奨されているその他の代替手段:req
require
mustinit
must
explicit
結論: ここでは、init
コンストラクター句を削除し、プロパティ修飾子として required
を進めます。
Init 句の制限
init
句機能は C# 11 では実装されませんでした。 これはアクティブな提案のままです。
init 句で this
へのアクセスを許可する必要がありますか? init
の割り当てをコンストラクター自体のメンバーを割り当てるための短縮形にしたい場合は、そうする必要があるようです。
さらに、base()
のように新しいスコープを作成するか、メソッド本体と同じスコープを共有しますか? これは、init句がアクセスする可能性のあるローカル関数や、init式がout
パラメーターを介して変数を導入することによる名前のシャドウイングに対処するために特に重要です。
結論: init
句は削除されました。
アクセシビリティの要件と init
init
句機能は C# 11 では実装されませんでした。 これはアクティブな提案のままです。
init
句を使用するこの提案のバージョンでは、次のシナリオを使用できることについて説明しました。
public class Base
{
protected required int _field;
protected Base() {} // Contract required that _field is set
}
public class Derived : Base
{
public Derived() : init(_field = 1) // Contract is fulfilled and _field is removed from the required members list
{
}
}
ただし、この時点で提案から init
句を削除したので、限られた方法でこのシナリオを許可するかどうかを決定する必要があります。 次のオプションがあります。
- シナリオを禁止します。 これは最も保守的なアプローチであり、アクセシビリティ の規則は現在、この前提を念頭に置いて記述されています。 規則としては、必要なメンバーは、最低限、その包含型と同じレベルの可視性を持つ必要があります。
- すべてのコンストラクターが次のいずれかである必要があります。
- 最も見えにくい必要メンバーと同程度にしか見えません。
SetsRequiredMembersAttribute
をコンストラクターに適用します。 これにより、コンストラクターを見ることができるすべてのユーザーが、エクスポートするすべてのものを設定することも、設定する必要もないことを確認できます。 これは、静的Create
メソッドまたは同様のビルダーを介してのみ作成された型に役立ちますが、ユーティリティは全体的に制限されているようです。
- 以前に LDM で説明されていたように、提案に対するコントラクトの特定の部分を削除する方法を再び追加します。
Conclusion: オプション 1、すべての必須メンバーが、少なくともそれらを含む型と同じように表示可能である,必要があります。
規則のオーバーライド
現在の仕様では、required
キーワードがコピーされる必要があり、そのオーバーライドによってメンバーの必須の度合いを高くすることはできますが、低くはできません。 それは私たちがやりたいことですか?
要件の削除を許可する場合は、現在提案しているよりも多くの契約変更機能が必要です。
結論: オーバーライドに required
を追加できます。 オーバーライドされたメンバーが required
されている場合は、オーバーライドされるメンバーも required
する必要があります。
代替メタデータ表現
また、拡張メソッドからページを取得して、メタデータ表現に別のアプローチを取ることもできます。 型に必要なメンバーが含まれていることを示す RequiredMemberAttribute
を型に配置し、必要な各メンバーに RequiredMemberAttribute
を配置できます。 これにより、参照シーケンスが簡略化されます (メンバー参照を行う必要はなく、属性を持つメンバーを探すだけです)。
結論: 代替案が承認されました。
メタデータ表現
メタデータ表現 を承認する必要があります。 さらに、これらの属性を BCL に含める必要があるかどうかを決定する必要があります。
RequiredMemberAttribute
の場合、この属性は nullable/nint/tuple メンバー名に使用する一般的な埋め込み属性と同じであり、C# のユーザーによって手動で適用されることはありません。 ただし、他の言語でこの属性を手動で適用したい場合もあります。- 一方、
SetsRequiredMembersAttribute
はコンシューマーによって直接使用されるため、BCL 内にある可能性があります。
前のセクションでの代替表現を使用すると、RequiredMemberAttribute
に対する考え方が変わる可能性があります。それは nint
/nullable/tuple メンバー名の一般的な埋め込み属性に似ているのではなく、拡張メソッドが出荷されてからフレームワークに含まれている System.Runtime.CompilerServices.ExtensionAttribute
に近いものです。
結論: 両方の属性を BCL に配置します。
警告とエラー
必要なメンバーを設定しない場合、警告にすべきかエラーにすべきか。 Activator.CreateInstance(typeof(C))
などを介してシステムをトリックすることは確かに可能です。つまり、すべてのプロパティが常に設定されていることを完全に保証できない可能性があります。 また、!
を使用してコンストラクター サイトで診断を抑制することもできます。一般的にエラーは許可されません。 ただし、この機能は読み取り専用フィールドや init プロパティに似ています。つまり、ユーザーが初期化後にこのようなメンバーを設定しようとすると、エラーが発生しますが、リフレクションによって回避される可能性があります。
結論: エラー。
C# feature specifications