required メンバー
Ver. 11
C# 11 でプロパティとフィールドに対する required
修飾子というものが追加されました。
これを使うと、オブジェクト初期化子で何らかの値を代入することを義務付けられます。
例えば以下のようなコードを書いたとき、a1
以外の new A
はエラーになります。
(警告ではなくエラーにします。)
var a1 = new A { X = "abc", Y = 123 }; var a2 = new A { X = "abc" }; // Y を代入していないのでエラー。 var a3 = new A { Y = 123 }; // X を代入していないのでエラー。 var a4 = new A(); // X も Y も代入していないのでエラー。 class A { public required string X { get; init; } public required int Y; }
この機能を指して、required メンバー (required members)と言います。
required の必要性
C# のオブジェクトの初期化には以下の2種類の構文があります。
-
new A(x, y)
: コンストラクターに引数で値を与える- 引数を並べる順序に意味があって、渡す先に仮引数名は指定しないので「位置指定」(positional)初期化と呼ぶ
-
new A { X = x, Y = y }
: オブジェクト初期化子でプロパティに値を与える- 順序に意味がなくて、プロパティ名は指定するので「名前指定」(nominal)初期化と呼ぶ
元々の C# にはコンストラクター(位置指定初期化)しかなかったのに対して、C# 3 でオブジェクト初期化子が導入されて名前指定初期化ができるようになりました。 C# 3 当時は名前指定初期化という考え方もなくて、あくまでコンストラクターの補助的な立ち位置でしたが、今となってはコンストラクターと対を成すような扱いを受けています。
クラスを作っている側で手間を惜しまないのであれば、普通にコンストラクターがある方が、使う側にとっては便利なことが多かったりします。 ただ、作る側の面倒は結構多いです。
まず、単にコンストラクターが増えるだけで手間。 よく言われる話ですが、プロパティ1個に対して同じような文字列を4回は繰り返す必要が出ます。
var a = new A("abc", 123); // 使う側は簡潔。 class A { public string X { get; } // ここに X を書いて public int Y { get; } public A(string x, int y) // ここにも x { X = x; // ここに至っては2個の X Y = y; } }
さらに、このクラス A
を継承して、もう1個 Z
プロパティを持った型 B
を作ることを考えます。
以下のように、さらに追加で2か所同じ文字列を追加する必要があります。
class A { // A の中身はさっきと一緒。 } // 派生クラスで1プロパティ増やしたくなった時 class B : A { public bool Z { get; } public B(string x, int y, bool z) // さらにここと、 : base(x, y) // ここにも x が必要。 { Z = z; } }
これに対して、名前指定初期化の場合はプロパティだけ書けばいいのでずいぶんと楽です。
// 使う側は多少長いものの、名前を明示してる分読みやすいかも。 var a = new B { X = "abc", Y = 123, Z = true, }; // クラス定義側は簡素に。 class A { public string X { get; init; } public int Y { get; init; } } class B : A { public bool Z { get; init; } }
ところがこれには1つ問題があります。
このコードの例で、X
プロパティのところに警告(CS8618)が出てしまっています。
この警告は null 許容参照型を有効化してるときにだけ発生するんですが、要するに、
「X
の型は (非 null な) string
なのに、有効な初期値を与えていない」というものです。
非 null な以上、何も値を与えない(勝手に null に初期化される)わけにはいきません。
そこで required
が導入されました。
「名前指定にはしたいけど、明示的な初期化も義務付けたい」という要件です。
var a = new A { X = "abc", // 非 null に初期化される保証がこの行でできる. Y = 123, }; // 明示的な初期化を義務付けたいプロパティ/フィールドには required を付ける。 // これを使えば null 許容参照型での問題も回避可能。 class A { public required string X { get; init; } public required int Y { get; init; } }
ちなみに、null 許容参照型は「わかりやすい需要の例」ではありますが、
別にその他の場面でも required
は使えます。
とにかく「初期化を明示させたい」というものなので、値型や null 許容型でも使えます。
// 全部 0 か null なので、別に new A() でも結果は同じものの、明示させたいという意図があるなら required。 var a1 = new A { X = null, Y = 0, Z = null }; var a2 = new A { X = null, Y = 0 }; // Z がないのでエラー。 class A { // default 値(0 や null)でもいいけども、とにかく明示はさせたい。 public required string? X { get; init; } public required int Y { get; init; } public required int? Z { get; init; } }
required の適用範囲
required
は、virtual
や abstract
なプロパティに対しても使えます。
ただし、基底クラス側が required
なものは派生クラス側にも required
を付ける必要があります。
abstract class A { public required abstract int X { get; init; } public required virtual int Y { get; init; } public virtual int Z { get; init; } } class B : A { // 基底クラス側が required なら、こっちも required でないとダメ。 public override required int X { get; init; } // 逆は大丈夫。基底クラスになくても、派生クラス側だけ required を足すことはできる。 public override required int Z { get; init; } } class C : A { // 派生側で required を取ってしまうとコンパイル エラー。 public override int X { get; init; } }
そして、required
はオブジェクト初期化で使うことが前提なので、
new
できないインターフェイスに対しては使えません。
interface I { // エラー。 required int X { get; init; } }
また、オブジェクト初期化子で値を渡せるように、
プロパティ/フィールドのアクセシビリティは、それを含む型よりも広い必要があります。
例えば、internal
クラスの internal
プロパティには使えますが、
public
クラスの protected
プロパティには使えません。
internal class A { // internal クラスの internal プロパティなので OK。 internal required int X { get; init; } } public class B { // public 未満のアクセシビリティは全部不可。以下は全部エラー。 protected required int X1 { get; init; } internal required int X2 { get; init; } internal protected required int X3 { get; init; } protected private required int X4 { get; init; } private required int X5; }
SetsRequiredMembers
required
メンバーをコンストラクター内で初期化するのであれば、
呼び出し元のオブジェクト初期化子では必ずしも初期化の必要がない場合があります。
こういう場合にエラーを出されても困るので、
SetsRequiredMembers
という属性(System.Diagnostics.CodeAnalysis
名前空間)を使って「このコンストラクターを呼んだ場合は required
メンバーの初期化をする必要はない」
という指定もできます。
using System.Diagnostics.CodeAnalysis; // required メンバーは A() (引数なしコンストラクター)で初期化するので、 // この場合は { X = "" } とかがなくてもエラーにならない。 var a = new A(); class A { public required string X { get; init; } public int Y { get; init; } [SetsRequiredMembers] public A() { X = "abc"; Y = 123; } }
ただ、この SetsRequiredMembers
は、利用側(呼び出した側)のエラーはなくしてくれる一方で、
作る側(コンストラクターの実装側)では特に何もしてくれません。
単にエラーを消します。
using System.Diagnostics.CodeAnalysis; // 自称 SetsRequiredMembers を信じてエラーは出さない。 var a = new A(); Console.WriteLine(a.X); // null class A { public required string X { get; init; } public int Y { get; init; } [SetsRequiredMembers] public A() { // 「requierd メンバーをセットする」と自称しているくせに、実際は何もしない。 // X に関しては nullability のフロー解析で、null 許容参照型警告が出るけども、全くの別件。 // Y に関しては一切何もチェックが働かない。 // 少なくとも C# 11 リリース時点では「仕様」(問題はわかっているものの、実装が大変なので妥協)。 // 現状の SetsRequiredMembers は「使う側はコンパイラーが守るけど、作る側は自分で頑張って」という姿勢。 } }
required メンバーの中身
required メンバーを含む型は、内部的には属性を付けて表現しているようです。 例えば、以下のようなクラスがあったとします。
class A { public required int X { get; init; } }
これをコンパイルすると、以下のようなコードに展開されます。
using System.Runtime.CompilerServices; [RequiredMember] class A { [RequiredMember] public int X { get; init; } [Obsolete("Constructors of types with required members are not supported in this version of your compiler.", true)] [CompilerFeatureRequired("RequiredMembers")] public A() { } }
型と、required メンバー自体には RequiredMember
属性(System.Runtime.CompilerServices
名前空間)が付いていて、これで required かどうかを判断しています。
そして、引数なしコンストラクターが追加されて、
そこに Obsolete
と CompilerFeatureRequired
属性が付きます。
これらは required メンバーに未対応の古いコンパイラーでこのクラスを使ったときにエラーにするための属性です。
これは本来どちらか片方でいいんですが、それぞれ以下のような用途です。
-
既存の仕組みでエラーにできるように
Obsolete
属性を付けている- required メンバーに対応しているコンパイラーの場合、「所定のメッセージの場合は無視してエラーにしない」みたいな特殊対応をしている
-
Obsolete
による対処は気持ち悪いので、「未対応ならエラー」のために新しいCompilerFeatureRequired
属性を作った- こちらは素直に、
featureName
引数に与えた文字列を見て対応できるかどうかを判定 CompilerFeatureRequired
に対応していないコンパイラーのサポートが切れるくらいの頃にObsolete
は消したい
- こちらは素直に、
init
の場合とは違って、modreq (属性よりも強い制約でコンパイル エラーにできる機構)は使わない方針です。
以下のような状況を考えると、制約が強い modreq は使いにくいそうです。
(不意に、コンパイラーが裏で勝手に作るコンストラクターが増えることがある。
不意に増えるものに使うには modreq は強すぎる。)
using System.Diagnostics.CodeAnalysis; class A { public required int X { get; init; } // SetsRequiredMembers なコンストラクターを明示。 // この場合、Obsolete, CompilerFeatureRequired 付きのコンストラクターはコンパイラー生成されない。 // もし、このコンストラクターを消すと… // コンパイラーが裏で Obsolete, CompilerFeatureRequired 付きを作ってしまう。 [SetsRequiredMembers] public A() { } }
演習問題
問題1
クラスの問題 1の Point
構造体および Triangle
クラスの各メンバー変数に対して、
プロパティを使って実装の隠蔽を行え。