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() { }
}

演習問題

問題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());
  }
}

更新履歴

ブログ