メモ
この記事は機能仕様についてです。 仕様は、機能の設計ドキュメントとして使用できます。 これには、提案された仕様の変更および機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が決定され、現在の ECMA 仕様に組み込まれるまで公開されます。
機能の仕様と行われた実装では、いくつかの違いがあることがあります。 これらの違いは、関連する言語設計ミーティング (LDM) メモに取り上げられています。
機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。
チャンピオンの課題: https://github.com/dotnet/csharplang/issues/39
まとめ
この提案では、init のみのプロパティとインデクサーの概念を C# に追加します。
これらのプロパティとインデクサーは、オブジェクトの作成時に設定できますが、オブジェクトの作成が完了した後にのみ、効果的に get
になります。
これにより、C# での不変モデルの柔軟性が大幅に高まります。
目的
C# で変更できないデータを構築するための基になるメカニズムは、1.0 以降変更されていません。 これらは次のままになります。
- フィールドを
readonly
として宣言。 get
アクセサーのみを持つプロパティの宣言。
これらのメカニズムは、変更できないデータの構築を許可する場合に効果的ですが、型の定型コードにコストを追加し、オブジェクト初期化子やコレクション初期化子などの機能からそのような型をオプトインします。 つまり、開発者は使いやすさと不変性のどちらかを選択する必要があります。
Point
のような単純な不変オブジェクトでは、型を宣言する場合と同じように、構築をサポートするために 2 倍のボイラー プレート コードが必要です。 タイプが大きくなるほど、このボイラープレートのコストも大きくなります。
struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
this.X = x;
this.Y = y;
}
}
init
アクセサーは、構築中に呼び出し元がメンバーを変更できるようにすることで、不変オブジェクトの柔軟性を高めます。 つまり、オブジェクトの不変プロパティはオブジェクト初期化子に参加できるため、型内のすべてのコンストラクタ ボイラープレートが不要になります。 Point
型がシンプルになりました。
struct Point
{
public int X { get; init; }
public int Y { get; init; }
}
その後、コンシューマーはオブジェクト初期化子を使用してオブジェクトを作成できます
var p = new Point() { X = 42, Y = 13 };
詳細な設計
init アクセサー
init only プロパティ (またはインデクサー) は、set
アクセサーの代わりに init
アクセサーを使用して宣言されます。
class Student
{
public string FirstName { get; init; }
public string LastName { get; init; }
}
init
アクセサーを含むインスタンス プロパティは、ローカル関数またはラムダの場合を除き、次の状況下で設定可能と見なされます。
- オブジェクト初期化子の間
with
式初期化子の間- 包含型または派生型に属するインスタンス コンストラクターの内部で、
this
またはbase
において this
またはbase
上の任意のプロパティのinit
アクセサー内- 名前付きパラメーターを使用した属性使用法内
このドキュメントでは、init
アクセサーが設定可能な上記の時間をまとめて、オブジェクトの構築フェーズと呼びます。
つまり、Student
クラスは次の方法で使用できます。
var s = new Student()
{
FirstName = "Jared",
LastName = "Parosns",
};
s.LastName = "Parsons"; // Error: LastName is not settable
init
アクセサーが設定可能な場合のルールは、型階層間で拡張されます。 メンバーがアクセス可能で、オブジェクトが構築フェーズにある場合、メンバーは設定可能です。 これにより、次のことが特に可能になります。
class Base
{
public bool Value { get; init; }
}
class Derived : Base
{
public Derived()
{
// Not allowed with get only properties but allowed with init
Value = true;
}
}
class Consumption
{
void Example()
{
var d = new Derived() { Value = true };
}
}
init
アクセサーが呼び出された時点で、インスタンスは未対応の構築段階であることが分かります。 そのため、init
アクセサーは、通常の set
アクセサーが実行できることに加えて、次のアクションを実行できます。
this
またはbase
で使用可能な他のinit
アクセサーを呼び出すthis
を使用して、同じ型で宣言されたreadonly
フィールドを割り当てる
class Complex
{
readonly int Field1;
int Field2;
int Prop1 { get; init; }
int Prop2
{
get => 42;
init
{
Field1 = 13; // okay
Field2 = 13; // okay
Prop1 = 13; // okay
}
}
}
init
アクセサーから readonly
フィールドを割り当てる機能は、アクセサーと同じ型で宣言されているフィールドに限定されます。 基本型の readonly
フィールドの割り当てには使用できません。 このルールにより、型の作成者は、その型の変更可能性の動作を制御できます。 init
を利用したくない開発者は、これを選択した他の種類の影響を受けません。
class Base
{
internal readonly int Field;
internal int Property
{
get => Field;
init => Field = value; // Okay
}
internal int OtherProperty { get; init; }
}
class Derived : Base
{
internal readonly int DerivedField;
internal int DerivedProperty
{
get => DerivedField;
init
{
DerivedField = 42; // Okay
Property = 0; // Okay
Field = 13; // Error Field is readonly
}
}
public Derived()
{
Property = 42; // Okay
Field = 13; // Error Field is readonly
}
}
仮想プロパティで init
を使用する場合は、すべてのオーバーライドも init
としてマークする必要があります。 同様に、単純な set
を init
でオーバーライドすることはできません。
class Base
{
public virtual int Property { get; init; }
}
class C1 : Base
{
public override int Property { get; init; }
}
class C2 : Base
{
// Error: Property must have init to override Base.Property
public override int Property { get; set; }
}
interface
宣言は、次のパターンを使用して init
スタイルの初期化に参加することもできます。
interface IPerson
{
string Name { get; init; }
}
class Init
{
void M<T>() where T : IPerson, new()
{
var local = new T()
{
Name = "Jared"
};
local.Name = "Jraed"; // Error
}
}
この機能の制限事項:
init
アクセサーは、インスタンス プロパティでのみ使用できます- プロパティに
init
アクセサーとset
アクセサーの両方を含めることはできません - ベースに
init
がある場合は、すべてのプロパティのオーバーライドにinit
が必要です。 このルールは、インターフェイスの実装にも適用されます。
Readonly 構造体
init
アクセサー (自動実装アクセサーと手動実装アクセサーの両方) は、readonly struct
のプロパティと readonly
プロパティで許可されます。 init
アクセサーは、readonly
型でも非readonly
readonly
型でも、自分自身を struct
としてマークすることは許可されていません。
readonly struct ReadonlyStruct1
{
public int Prop1 { get; init; } // Allowed
}
struct ReadonlyStruct2
{
public readonly int Prop2 { get; init; } // Allowed
public int Prop3 { get; readonly init; } // Error
}
メタデータ エンコード
プロパティ init
アクセサーは、戻り値の型が IsExternalInit
の modreq でマークされた標準の set
アクセサーとして出力されます。 これは、次の定義を持つ新しい型です。
namespace System.Runtime.CompilerServices
{
public sealed class IsExternalInit
{
}
}
コンパイラは型を完全な名前で識別します。 コア ライブラリに表示する要件はありません。 この名前で複数の型がある場合、コンパイラは次の順序でタイブレークを行います。
- コンパイル中のプロジェクトで定義されたもの
- corelib で定義されたもの
これらのいずれかが存在しない場合は、型の不明瞭なエラーが表示されます。
IsExternalInit
の設計は、この号 の でさらに詳しく説明されています。
質問
重大な変更
この機能がどのようにエンコードされるのかに関する主な軸のひとつは次の問いに帰結します。
init
をset
に置き換えるのはバイナリ破壊的変更か?
init
を set
に置き換え、プロパティを完全に書き込み可能にすることは、非仮想プロパティのソースの破壊的変更になることはありません。 プロパティを記述できるシナリオ一式を拡張するだけです。 問題になっている唯一の挙動は、これがバイナリ破壊的変更のままであるかどうかです。
init
からset
への変更をソースおよびバイナリ互換にするためには、modreqと属性に関する決定を下すことが必要になります。これは、modreqをソリューションとして除外するためです。 一方、これが興味深いものではないとみなされた場合、modreq 対属性の判断はあまり意味がなくなります。
解決方法 このシナリオは LDM では説得力があるとは見なされません。
Modreqs と属性
init
プロパティ アクセサーの出力戦略では、メタデータ中に出力するときに属性または modreqs を使用するかを選択する必要があります。 これらは考慮する必要がある異なるトレードオフを持っています。
modreq 宣言を使用してプロパティ セット アクセサーに注釈を付けるということは、CLI 準拠のコンパイラが modreq を認識しない限り、アクセサーを無視することを意味します。 init
を認識しているコンパイラだけがメンバーを読み取るということです。 init
を認識していないコンパイラは、set
アクセサーを無視するため、プロパティが誤って読み取り/書き込みされません。
modreq の欠点は、init
が set
アクセサーのバイナリ署名の一部になることです。 init
を追加または削除すると、アプリケーションのバイナリ互換性が損なわれます。
属性を使用して set
アクセサーに注釈を付けるということは、属性を理解しているコンパイラだけがアクセスを制限することを認識することを意味します。 init
を認識しているコンパイラは、それを単純に読み取り/書き込み プロパティとみなし、アクセスを許可します。
これは一見、バイナリーの互換性を犠牲にして安全性を高めるかどうかを選択するように見えます。 少し掘り下げてみると、追加の安全性は見た目通りではないことがわかります。 次の状況では保護されません。
public
メンバーに対するリフレクションdynamic
の使用- modreqs を認識しないコンパイラ
また、.NET 5 の IL 検証ルールを完了すると、init
がそれらのルールの 1 つになることも考慮する必要があります。 つまり、検証可能な IL を出力するコンパイラを検証するだけで、追加の適用が得られます。
.NET (C#、F#、VB) の主言語はすべて、これらの init
アクセサーを認識するように更新されます。 したがって、ここでの唯一の現実的なシナリオは、C# 9 コンパイラが init
プロパティを出力し、C# 8、VB 15 ... C# 8 などの古いツールセットで表示される場合です。 これは、バイナリの互換性を考慮し、比較検討する必要があるトレードオフです。
注意 この説明は、主にメンバーにのみ適用され、フィールドには適用されません。 init
フィールドは LDM によって拒否される一方で、modreq と属性は、会議の余地があります。 フィールドの init
機能は、readonly
の既存の制限を緩和します。 つまり、readonly
+ 属性としてフィールドを出力した場合、古いコンパイラがフィールドを誤って使用するリスクはありません。これは、既に readonly
が認識されているためです。 したがって、ここで modreq を使用しても、追加の保護は追加されません。
解決策 この機能では、modreq を使用して プロパティ init
setter をエンコードします。 説得力のある要因として次が挙げられます (特定の順序はありません)。
- 古いコンパイラが
init
セマンティクスに違反するのを防ぐ virtual
宣言内のinit
またはinterface
追加または削除することは、ソースとバイナリ両方にとって破壊的変更です。
init
の削除をバイナリ互換の変更とするための重要なサポートもなかったことから、modreq の使用を選択することが簡単でした。
init と initonly
LDM 会議中に重要な考慮事項を取得した 3 つの構文形式がありました。
// 1. Use init
int Option1 { get; init; }
// 2. Use init set
int Option2 { get; init set; }
// 3. Use initonly
int Option3 { get; initonly; }
解決策 LDM には圧倒的に好まれる構文はありませんでした。
将来の一般的な機能として init
メンバーを実現できるかどうかに構文の選択が与える影響が重要な注目を集めた点の一つでした。
オプション 1 を選択すると、将来、init
スタイルの get
メソッドを持つプロパティを定義することが困難になります。 最終的に、将来において一般的な init
メンバーと共に進めていくことが決定された場合、init
をプロパティ アクセサー リストの修飾子として使用し、さらに init set
の省略形とすることができると決定されました。 基本的に、次の 2 つの宣言は同じになります。
int Property1 { get; init; }
int Property1 { get; init set; }
プロパティ アクセサー リストのスタンドアロン アクセサーとして init
を使用して前進することを決定しました。
失敗した init に対する警告
次のシナリオで考えてみましょう。 型は、コンストラクターで設定されていない init
メンバーのみを宣言します。 オブジェクトを構築するコードが値の初期化に失敗した場合、警告を受け取る必要がありますか?
その時点で、フィールドが設定されることは永遠にないことが明白であり、そのため、private
データを初期化できなかった場合の警告と多くの共通点があります。
したがって、警告には外見的に何らかの値がありますか?
ただし、この警告には大きな欠点があります。
- これは、
readonly
をinit
に変更する互換性の話を複雑にします。 - 呼び出し元が初期化する必要があるメンバーを示すために、追加のメタデータを保持する必要があります。
さらに、オブジェクト作成者に特定のフィールドに関する警告/エラーを強制する全体的なシナリオに価値があると考えられる場合、これは一般的な機能として理にかなっている可能性があります。 init
メンバーのみに限定する必要はありません。
解決策 init
フィールドとプロパティの使用に関する警告は発生しません。
LDM は、必要なフィールドとプロパティの考え方についてより広範な議論をしたいと考えています。 その結果、init
メンバーと検証に関する立場を再考する場合があります。
フィールド修飾子として init を許可する
init
がプロパティ アクセサーとして機能するのと同じ方法で、フィールドの指定として機能し、init
プロパティと同様の動作を提供することもできます。
これにより、型、派生型、またはオブジェクト初期化子によって構築が完了する前にフィールドを割り当てることができます。
class Student
{
public init string FirstName;
public init string LastName;
}
var s = new Student()
{
FirstName = "Jarde",
LastName = "Parsons",
}
s.FirstName = "Jared"; // Error FirstName is readonly
メタデータでは、これらのフィールドは readonly
フィールドと同じ方法でマークされますが、追加の属性または modreq を使用すると init
スタイル フィールドであることを示すことができます。
決議 LDMは、この提案が妥当であると同意しましたが、全体的にシナリオは特性から逸脱していると感じました。 ここでは、init
プロパティのみを使用することが決定されました。
これは、init
プロパティがプロパティの宣言型の readonly
フィールドを変更できるため、適切なレベルの柔軟性を保つことができます。 これは、シナリオを正当化する重要な顧客フィードバックがある場合に再検討されます。
型修飾子として init を許可する
readonly
修飾子を struct
に適用してすべてのフィールドを readonly
として自動的に宣言するのと同じ方法で、init
のみの修飾子を struct
または class
に宣言して、すべてのフィールドを自動的に init
としてマークできます。
これは、次の 2 つの型宣言が同等であることを意味します。
struct Point
{
public init int X;
public init int Y;
}
// vs.
init struct Point
{
public int X;
public int Y;
}
解決策 この機能はかわいすぎるため、ベースとなっている readonly struct
機能と競合します。 readonly struct
機能は、フィールド、メソッドなど、すべてのメンバーに readonly
を適用するという点で簡単です。init struct
機能はプロパティにのみ適用されます。 これは実際には、ユーザーが混乱する結果となります。
init
が型の特定の側面でのみ有効であることを考えると、型修飾子として保持するという考え方をやめました。
考慮事項
互換性
init
機能は、既存の get
のみのプロパティと互換性を持つよう設計されています。 具体的には、現在は get
のみのプロパティに対して、より柔軟なオブジェクト作成のセマンティクスを持たせるために完全に追加される変更を意味しています。
たとえば、次の型について考えます。
class Name
{
public string First { get; }
public string Last { get; }
public Name(string first, string last)
{
First = first;
Last = last;
}
}
これらのプロパティに init
を追加することは、非破壊的変更です。
class Name
{
public string First { get; init; }
public string Last { get; init; }
public Name(string first, string last)
{
First = first;
Last = last;
}
}
IL 検証
.NET Core が IL 検証の再実装を決定した場合、init
メンバーを考慮するようにルールを調整する必要があります。 これは、readonly
データへの非変更アクセスのルール変更に含める必要があります。
IL 検証ルールは、次の 2 つに分割する必要があります。
init
のメンバーがreadonly
フィールドを設定できるようにします。init
メンバーを法的に呼び出すことができるタイミングを決定する。
1 つ目は、既存のルールを簡単に調整することです。 IL 検証ツールは init
メンバーを認識するように学習でき、そこから必要なのは、そのようなメンバーの this
に readonly
フィールドを設定可能であると考慮することだけです。
2 番目のルールはより複雑です。 オブジェクト初期化子の単純なケースでは、ルールは簡単です。 new
式の結果がまだスタック上にある場合は、init
メンバーを法的に呼び出せます。 つまり、値がローカル、配列要素、またはフィールドに格納されるまで、または別のメソッドに引数として渡されるまで、init
メンバーを呼び出すことができます。 これにより、new
式の結果が (this
以外の) 名前付き識別子に発行されると、init
メンバーを法的に呼び出せなくなります。
より複雑なケースは、init
メンバーとオブジェクト初期化子およびawait
を混在する場合です。 これにより、新しく作成されたオブジェクトがステート マシンに一時的にホイストされ、フィールドに配置される場合があります。
var student = new Student()
{
Name = await SomeMethod()
};
ここでは、Name
の一湿気が発生する前に、フィールドとしてステート マシンにnew Student()
の結果をホイストします。 コンパイラは、IL 検証ツールがユーザーがアクセスできないため、init
の意図されたセマンティクスに違反していないことを把握するように、このようなホイスト フィールドをマークする必要があります。
init メンバー
init
修飾子は、すべてのインスタンス メンバーに適用するように拡張できます。 これにより、オブジェクトの構築中に init
の概念が一般化され、構築プロセスに参加できるヘルパー メソッドを型が宣言して、init
フィールドとプロパティを初期化できるようにします。
このようなメンバーには、init
アクセサーがこの設計で行うすべての制限があります。 しかし、この必要性には疑問が残り、言語の将来のバージョンでは互換性のある方法でこれを安全に追加することができます。
3 つのアクセサーを生成する
init
プロパティの潜在的な実装の 1 つとして、init
を set
から完全に分離することが挙げられます。 つまり、プロパティには、get
、set
、init
の 3 つのアクセサーが存在する場合があります。
これには、バイナリの互換性を維持しながら、modreq を使用して正確性を適用できるという潜在的な利点があります。 実装は大まかに次のようになります。
set
がある場合、init
アクセサーは常に出力されます。 開発者が定義していない場合、それは単にset
への参照です。- オブジェクト初期化子のプロパティ一式は、常に
init
(存在する場合) を使用しますが、存在しない場合はset
にフォールバックします。
つまり、開発者は常にプロパティから init
を安全に削除できます。
この設計の欠点は、init
があるときに がset
出力される場合にのみ有用であることです。 言語は、init
が過去に削除されたかどうかを把握できず、それを想定する必要があるため、init
を常に出力する必要があります。 これにより、メタデータの大幅な拡張が発生するため、この時点では、単純に互換性のコストに釣り合いません。
C# feature specifications