「Rosly の Language Feature Status にこの1・2か月で結構更新かかったね」という話題もたびたびあり、その辺りの話を。

Language Feature Status に並んでいるもののうち、いくつかは preview として現時点でもうすでに取り込まれています。

  • field キーワード ← 今日はこれ
  • First-class Span
  • nameof(T<>)

今(執筆時、Visual Studio 17.13.0 Preview 2.1)の時点でも、 LangVersionpreview を指定すれば利用可能です。

最初は3つまとめて1ブログにしようかと思ってたんですが、 案外長くなったので個別に。 今日は field キーワードの話になります。 (昔のブログを参照して「やっと入ったよ」だけ書いて終わりかと思ったら案外新規に書くことがあり。)

field キーワード

プロパティ内において field をキーワード扱いして、 「プロパティのバッキング フィールドを表す変数」にしようという案があって、 今は機能名としても「field キーワード」と呼ばれています。

class A(int x)
{
    // (既存の)自動プロパティ。
    public int X1 { get; set; }

    // X1 と同じ意味になる「field キーワード持ち」のプロパティ。
    public int X2
    {
        get => field;
        set => field = value;
    }

    // 片方を自動、片方を field 持ちにもできる。
    public int X3
    {
        get; // 自動。
        set => field = value; // field 持ち。
    }

    // 自動プロパティでできたことは一通りこっちでもできる。
    // (イニシャライザーも持てたり、get-only とか init とかも。)
    // (というか、扱いは完全に自動プロパティと同じ。)
    public int X4 { get => field; } = x;

    // get 省略形の => 内でも field が使える。
    // int X5 { get; } と全く同じ。(コンストラクターで初期化可能。)
    public int X5 => field;
}

2年前にすでに「場合によっては C# 11 に入っていたかも」と言っていたものがようやく C# 14 で入ります。 当時は「半自動プロパティ」(semi-auto properties)とか呼んでいましたが、 結局「field キーワード」で行こうという感じになっているみたいです。

Visual Studio 17.12 Preview 3 / .NET 9 RC 2 の頃にはすでに merge されています。 つまり、C# 13 正式リリース(.NET 9)よりも前に、 すでに C# 14 の preview 機能が取り込まれている状態。 結構長いこと検討していて実装もあるものの、いくつか懸念があって延びに延びていて、 ようやく preview として世に出すことに。

懸念の1つは、これが「そこそこありえる」頻度の破壊的変更になることです。 「field という名前のフィールドがあって、this. は付けずに、プロパティの中で参照している」という状況が破壊的変更になります。 (「そこまで多くはないけど、まあそういう人も一定数いる」レベル。)

class A
{
    int field;

    public int Field
    {
        // C# 13 まで: field フィールドの参照。
        // C# 14 から: field キーワード。
        //             field フィールドはノータッチになる。
        //             field フィールドを参照したければ @field とか this.field にする。
        get => field;
        set => field = value;
    }
}

このコードは一応、C# 14 では警告になる予定です。 「field キーワードが field フィールドを隠してるけども意図通りか?」と怒られて、@field への書き換えを推奨されます。 もしかすると、今年のうち(C# 13 の間)に、「今のうちから @field に書き換えておいてくれ」アナライザーが提供されるかもしれません。

ちなみに、プロパティ内において、field は完全にキーワードになっています。 当初は「既存のコードを壊さない限りにキーワード扱いする」みたいな努力をするかどうかという話もあったんですが、複雑すぎるので断念しています。 例えば、field という名前のローカル変数があったとしてもキーワード扱いです。

class A
{
    public int X
    {
        get
        {
            var field = 1;
            return field; // これは field キーワード。フィールドの場合と同じく警告あり。
        }
    }
}

nameof(field) もエラーになります。 nameof(int) とかがエラーなのと同じ。

class A
{
    public string X
    {
        get => nameof(field); // ダメ。
    }
}

(余談で、value もキーワードに変えちゃうかという話もあったんですが、これは没になりました。)

これと関連して、以下のようなコードを書くと、タプル要素名のやつだけエラーを起こします。

class A
{
    public int X
    {
        get
        {
            var x = (field: 1, 2); // タプル要素名 (これだけコンパイル エラー)
            var y = new { field = 1 }; // 匿名型のプロパティ
            var z = new Foo() { field = 1 }; // オブジェクト初期化子でのフィールド/プロパティ参照
            if (y is { field: 1 }) { } // プロパティ パターンでのフィールド/プロパティ参照

            return field;
        }
    }

    class Foo
    {
        public int field;
    }
}

最後にもう1つ、null 許容参照型のフロー解析の問題があります。 プロパティが T のとき、そのバッキング フィールド(field キーワードの実体)は T であるべきか、T? であるべきか。

例えば以下のような ??= を使った遅延初期化コードはよく書くと思います。

class A(Type type)
{
    // Type.Name のキャッシュ。
    public string Name
    {
        // 遅延初期化にしたいので field ??= で代入。
        get => field ??= type.Name;
    }
}

現状(Visual Studio 17.13.0 Preview 2.1 時点)、「プロパティが T なら fieldT」です。 この例の場合、string (not null)。 「not null なフィールドがあるのに、コンストラクターで初期化していない」という警告が出ます。

解決策は検討さいれているんですが、短期的には MaybeNull 属性を使って回避してくれと言われています。

using System.Diagnostics.CodeAnalysis;

class A(Type type)
{
    [field: MaybeNull] // この属性によって、field が string? 扱いになる。
    public string Name
    {
        get => field ??= type.Name;
    }
}

上記解決策が間に合うなら、 「いったん fieldT? と仮定してフロー解析して nullable 警告を起こすかどうか」をみてバッキング フィールドが TT? かを決定するとのこと。 これが入れば MaybeNull を付ける前のコードでも警告が出なくなる予定です。