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 は、virtualabstract なプロパティに対しても使えます。 ただし、基底クラス側が 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 かどうかを判断しています。

そして、引数なしコンストラクターが追加されて、 そこに ObsoleteCompilerFeatureRequired 属性が付きます。 これらは 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() { }
}

field キーワード

Ver. 14

自動プロパティではバッキング フィールドへの値の素通しが行われます。 これに対して、ちょこっとだけ実装をいじりたいことが結構あります。 特によくあるのが「バッキング フィールドの生成は自動でやってほしいけど、get/set の中身は自分で書きたい」という状況で、例えば下のような例があります。

using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;

class FieldBackedProperties : INotifyPropertyChanged
{
    // 遅延初期化: 最初のアクセス時にインスタンスを生成。
    private string? _x;
    public string X => _x ??= "";

    // set 側だけ null 許容(get 側で ?? で非 null 化)。
    private string? _y;

    [AllowNull]
    public string Y
    {
        get => _y ?? "";
        set => _y = value;
    }

    // INotifyPropertyChanged の実装: get 側だけ素通し。
    private string? _z;

    public string? Z
    {
        get => _z;
        set
        {
            if (_x != value)
            {
                _z = value;
                PropertyChanged?.Invoke(this, new(nameof(Z)));
            }
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;
}

これに対して C# 14 では、 field キーワードというものを追加しました。 プロパティの get/set の中に field と書くと、 バッキング フィールドを生成した上で、そのフィールドの読み書きができます。 例えば前述の例を field を使って書き直すと以下のようになります。

using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;

class FieldBackedProperties : INotifyPropertyChanged
{
    // 遅延初期化: 最初のプロパティ アクセス時にインスタンスを生成。
    public string X => field ??= "";

    // set 側だけ null 許容(get 側で ?? で非 null 化)。
    [AllowNull]
    public string Y
    {
        get => field ?? "";
        set;
    }

    // INotifyPropertyChanged の実装: get 側だけ素通し。
    public string? Z
    {
        get;
        set
        {
            if (field != value)
            {
                field = value;
                PropertyChanged?.Invoke(this, new(nameof(Z)));
            }
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;
}

field キーワードには以下のようなメリットがあります。

  • 重複を避けれる
    • この例の場合は _x みたいな短い名前なものの、プロパティ名はもっと長いことが多いので繰り返したくない
    • プロパティの型も、型名が長いことが多々ある
  • 他のプロパティから参照されるのを避けれる
    • ほとんどの場合「_xX 内でしか使わない」みたいなことになるのに、_x が他のメソッドやプロパティから見えてしまっていた

ちなみに(この例で既に使っていますが)、自動実装(空っぽの get/set)との併用もできます。 get;get => field; と、 set;set => field = value; と同じ意味になります。

自動プロパティとの共通点

既存の自動プロパティと、 C# 14 で追加された field キーワードを使ったプロパティは 「バッキング フィールドが自動生成される」という意味で共通しているわけですが、 これらを合わせて field-baked プロパティ(フィールドで裏付けされたプロパティ)と呼びます。 ひとくくりにする言葉が用意されているくらいにはこの2つは扱われ方が似ています。

以下は一例ですが、「get だけ書くと get-only プロパティになる」という挙動は完全に一致します。

class GetOnly
{
    // 元々ある get-only プロパティ。
    public int X { get; }

    // get => field; と get; は全く同じ意味で、これも get-only プロパティになる。
    public int Y { get => field; }

    // 何ならこれも get => field; の省略形なので get-only プロパティになる。
    public int Z => field;

    // 中身をカスタマイズしても、field キーワードを使っている時点で get-only プロパティ。
    public int W => field + 1;

    public GetOnly(int x, int y, int z, int w)
    {
        // なので set; を省略していても、コンストラクター内に限り値の代入が可能。
        // (バッキング フィールドへの直代入扱い。)
        X = x;
        Y = y;
        Z = z;
        W = w;
    }
}

他の例として、ref 付きのバッキング フィールドは作れないという制限も共通です。

ref struct RefField
{
    // ref 付きのプロパティは自動実装にできない。
    public ref int X { get; }

    // 同じく field キーワードは使えない。
    public ref int Y => ref field;

    // 参考: これなら書ける。(警告は別件。)
    private ref int _z;
    public ref int Z => ref _z;
}

文脈キーワード

field 「キーワード」とは言っていますが、 他の例にもれず field文脈キーワードです。 プロパティの get/set 内でだけキーワード扱いされます。

class A
{
    // これは普通にフィールド。
    private int field;

    public int M()
    {
        // これは普通にローカル変数。
        var field = 123;
        return field;
    }

    // これは文脈キーワードの field。
    // (ちなみにこの例では「同名のフィールドがあるけど大丈夫?」と警告される。)
    public int X => field;
}

// これも警告は出るものの合法。普通に型名。
// (「小文字アルファベット始まるの型名は将来の文脈キーワードと被る可能性が高いからやめてほしい」という警告。)
class field;

class B
{
    // こんなのすら合法。
    public field field(field field) => field;
}

この例のような「field という名前のフィールド」は元々書けていたわけで、 field キーワードの追加はたとえ文脈キーワードだとしても破壊的変更です。 以下のコードは C# 13 と 14 で解釈が異なります。

class A
{
    private int field;

    // C# 13: field フィールドを参照。
    // C# 14: X のバッキング フィールドが自動生成されて、それを参照。
    //        (field フィールドとは別のフィールドが生成される。)
    public int X => field;

    // 以前の挙動を得るためには:

    // @ を付けるとキーワードではなくなる。この名前のフィールドを参照。
    public int Y => @field;

    // this. を付けてもフィールド参照にできる。
    public int Z => this.field;
}

プロパティ初期化子

プロパティ初期化子を使う場合ちょっと注意が必要になります。 初期化子で値を渡す場合、プロパティの set アクセサー呼び出しではなく、バッキング フィールドへの直代入になります。

var x = new PropertyInitializer(10);

// x.X は 10 になる。
// set が呼ばれていなくて、バッキング フィールドに直接 10 が渡る。
Console.WriteLine(x.X);

class PropertyInitializer(int x)
{
    public int X
    {
        get;
        set => field = value + 1; // 値を1ずらす
    } = x;
}

コンストラクターの場合はこんなことはなくて、ちゃんと set アクセサーが呼ばれます。

var x = new Constructor(10);

// x.X は 11 になる。
// ちゃんと set 経由でバッキング フィールドの初期化が行われる。
Console.WriteLine(x.X);

class Constructor
{
    public int X
    {
        get;
        set => field = value + 1; // 値を1ずらす
    }

    public Constructor(int x)
    {
        X = x; // この場合は set アクセサーが呼ばれる。
    }
}

変な挙動ではありますが、これは初期化子やコンストラクターの実行順序に関係しています。 「コンストラクター」や 「継承」で説明していますが、フィールド初期化子やプロパティ初期化子でインスタンス メソッドを呼べてしまうと、未初期化のフィールドを読んでしまう可能性があります。 プロパティのアクセサーの実態はメソッドとほぼ同じなので同様の問題があり得て、 初期化子で set アクセサーは呼んではいけないということになります。 そのため仕方なく、プロパティ初期化子ではフィールドへの直代入する仕様になっています。

バッキング フィールドの null 許容性

プロパティが参照型のとき、そのバッキング フィールドの null 許容性はどうあるべきでしょうか? 本節冒頭の例でも挙げたように、field キーワードの用途の1つに遅延初期化があります。 この場合、「T 型のプロパティのバッキング フィールドは T? の方が都合がいい」ということになります。

class LazyInit
{
    // field は string? でも大丈夫。
    // 一方で、field が string だとすると「コンストラクターで非 null に初期化しろ」警告が出るはず。
    // つまり、field は string? の方が都合がいい。
    public string X => field ?? "";
}

かといって常に T? にすればいいというものでもなく、T でないとまずい場合もあります。 ちょっと複雑な例ですが、以下のコードを見てください。

using System.Diagnostics.CodeAnalysis;

class AllowNullSetter
{
    // AllowNull を付けると set 側だけ nullable になる。
    // obj.X = null; を渡せて、でも、var x = obj.X; は null にならない。

    // フィールドは string? であってほしい例: 
    [AllowNull]
    public string X
    {
        get => field ?? ""; // こっちで非 null を保証。
        set => field = value;
    }

    // フィールドは string であってほしい例: 
    [AllowNull]
    public string Y
    {
        get => field;
        set => field = value ?? ""; // こっちで非 null を保証。
    } = "";
}

これをコンパイラーが正しく判断できるように、get/set 両方合わせてフロー解析する仕様になっています (通常、null 許容性のフロー解析は2つ以上のメソッドをまたいで行いません。 get/set の中身はそれぞれ独立したメソッドなので、ここだけの特殊処理になります)。 get 側で fieldT? だと思ってフロー解析してみて警告にならなかった場合、 set 側も fieldT? かもしれない前提でフロー解析します。

class Nullability
{
    public string X
    {
        get => field ?? ""; // field は string? でも問題ない。
        set
        {
            // string? 扱いでフロー解析。
            string x = field; // ここで警告。
        }
    }

    public string Y // ここに「非 null 初期化しろ」警告が出る。
    {
        get => field; // field は string でないとおかしい。
        set
        {
            // string 扱いでフロー解析。
            string x = field; // 警告なし。
        }
    }

    public string Z
    {
        get => field ?? "";
        set
        {
            // string? 扱いでフロー解析するとしても、
            // value が string なのでここより後ろでは field は非 null。
            field = value;
            string x = field; // 警告なし。
        }
    }

    public string W
    {
        set
        {
            // ちなみに get を省略すると field は string? 扱いになる。
            string x = field; // ここで警告。
        }
    }
}

ちなみにこの挙動はあくまで null 許容参照型に対するものです。 null 許容値型の場合は「T 型プロパティのバッキング フィールドは常に T」になります。 int X => field ??= 1; などと書くとエラー(fieldint? にはならず intint に対して ?? は使えない)になります。

演習問題

問題1

クラス問題 1Point 構造体および Triangle クラスの各メンバー変数に対して、 プロパティを使って実装の隠蔽を行え。

解答例1

using System;

/// <summary>
/// 2次元の点をあらわす構造体
/// </summary>
struct Point
{
  double x; // x 座標
  double y; // y 座標

  #region 初期化

  /// <summary>
  /// 座標値 (x, y) を与えて初期化。
  /// </summary>
  /// <param name="x">x 座標値</param>
  /// <param name="y">y 座標値</param>
  public Point(double x, double y)
  {
    this.x = x;
    this.y = y;
  }

  #endregion
  #region プロパティ

  /// <summary>
  /// x 座標。
  /// </summary>
  public double X
  {
    get { return this.x; }
    set { this.x = value; }
  }

  /// <summary>
  /// y 座標。
  /// </summary>
  public double Y
  {
    get { return this.y; }
    set { this.y = value; }
  }

  #endregion

  public override string ToString()
  {
    return "(" + x + ", " + y + ")";
  }
}

/// <summary>
/// 2次元空間上の三角形をあらわす構造体
/// </summary>
class Triangle
{
  Point a;
  Point b;
  Point c;

  #region 初期化

  /// <summary>
  /// 3つの頂点の座標を与えて初期化。
  /// </summary>
  /// <param name="a">頂点A</param>
  /// <param name="b">頂点B</param>
  /// <param name="c">頂点C</param>
  public Triangle(Point a, Point b, Point c)
  {
    this.a = a;
    this.b = b;
    this.c = c;
  }

  #endregion
  #region プロパティ

  /// <summary>
  /// 頂点A。
  /// </summary>
  public Point A
  {
    get { return a; }
    set { a = value; }
  }

  /// <summary>
  /// 頂点B。
  /// </summary>
  public Point B
  {
    get { return b; }
    set { b = value; }
  }

  /// <summary>
  /// 頂点C。
  /// </summary>
  public Point C
  {
    get { return c; }
    set { c = value; }
  }

  #endregion

  /// <summary>
  /// 三角形の面積を求める。
  /// </summary>
  /// <returns>面積</returns>
  public double GetArea()
  {
    double abx, aby, acx, acy;
    abx = b.X - a.X;
    aby = b.Y - a.Y;
    acx = c.X - a.X;
    acy = c.Y - a.Y;
    return 0.5 * Math.Abs(abx * acy - acx * aby);
  }
}

/// <summary>
/// Class1 の概要の説明です。
/// </summary>
class Class1
{
  static void Main()
  {
    Triangle t = new Triangle(
      new Point(0, 0),
      new Point(3, 4),
      new Point(4, 3));

    Console.Write("{0}\n", t.GetArea());
  }
}

更新履歴

ブログ