次の方法で共有


自動デフォルト構造体

手記

この記事は機能仕様です。 仕様は、機能の設計ドキュメントとして機能します。 これには、提案された仕様の変更と、機能の設計と開発時に必要な情報が含まれます。 これらの記事は、提案された仕様の変更が最終決定され、現在の ECMA 仕様に組み込まれるまで公開されます。

機能の仕様と完成した実装の間には、いくつかの違いがある可能性があります。 これらの違いは、関連する 言語設計会議 (LDM) ノートでキャプチャされます。

機能仕様を C# 言語標準に導入するプロセスの詳細については、仕様に関する記事を参照してください。

https://github.com/dotnet/csharplang/issues/5737

概要

この機能により、構造体コンストラクター内で、ユーザーが戻る前または使用する前に明示的に割り当てられていないフィールドを識別し、明確な割り当てエラーを発生させる代わりに、それらをdefaultに暗黙的に初期化します。

モチベーション

この提案は、dotnet/csharplang#5552 および dotnet/csharplang#5635 で見つかった使いやすさの問題の軽減策として、および #5563 への対処として発生します (すべてのフィールドを確実に割り当てる必要がありますが、field はコンストラクター内でアクセスできません)。


C# 1.0 以降、構造体コンストラクターは、out パラメーターであるかのように this を確実に割り当てる必要がありました。

public struct S
{
    public int x, y;
    public S() // error: Fields 'S.x' and 'S.y' must be fully assigned before control is returned to the caller
    {
    }
}

これは、コンパイラがプロパティの割り当てをバッキング フィールドの割り当てと同等として処理できないため、セッターが手動で半自動プロパティで定義されている場合に問題が発生します。

public struct S
{
    public int X { get => field; set => field = value; }
    public S() // error: struct fields aren't fully assigned. But caller can only assign 'this.field' by assigning 'this'.
    {
    }
}

セッターが ref this を受け取らず、パラメーターとして out field を受け取るスキームなど、セッターに細かい制限を導入することは、一部のユース ケースではあまりにもニッチで不完全であると想定しています。

私たちが苦労している基本的な緊張の1つは、構造体プロパティが手動でセッターを実装している場合、ユーザーは多くの場合、ロジックを繰り返し割り当てるか繰り返すという何らかの形式の"繰り返し"を行う必要があるということです。

struct S
{
    private int _x;
    public int X
    {
        get => _x;
        set => _x = value >= 0 ? value : throw new ArgumentOutOfRangeException();
    }

    // Solution 1: assign some value in the constructor before "really" assigning through the property setter.
    public S(int x)
    {
        _x = default;
        X = x;
    }

    // Solution 2: assign the field once in the constructor, repeating the implementation of the setter.
    public S(int x)
    {
        _x = x >= 0 ? x : throw new ArgumentOutOfRangeException();
    }
}

前のディスカッション

小さなグループがこの問題を見て、考えられるいくつかの解決策を検討しました。

  1. セミオート プロパティでセッターが手動で実装されている場合は、ユーザーに this = default の割り当てを要求します。 フィールド初期化子に設定された値が吹き飛ばされるため、これは間違った解決策です。
  2. auto/semi-auto プロパティのすべてのバッキング フィールドを暗黙的に初期化します。
    • これにより、「半自動プロパティ セッター」の問題が解決され、明示的に宣言されたフィールドが、「フィールドを暗黙的に初期化しないで、自動プロパティを暗黙的に初期化する」という異なるルールの下に直接配置されます。
  3. 準自動プロパティのバッキング フィールドを割り当てる方法を指定し、ユーザーに割り当てる必要があります。
    • これは(2)と比較して面倒な場合があります。 auto プロパティは "automatic" と想定されており、フィールドの "自動" 初期化が含まれている可能性があります。 基になるフィールドがプロパティへの割り当てによって割り当てられている場合や、プロパティ セッターが呼び出されている場合に混乱が生じる可能性があります。

ユーザーから、たとえば、明示的にすべてを割り当てることなく、いくつかのフィールド初期化子を構造体に含めたいというフィードバック を受け取りました。 この問題と、"手動で実装されたセッターを持つ準自動プロパティ" の問題を同時に解決できます。

struct MagnitudeVector3d
{
    double X, Y, Z;
    double Magnitude = 1;
    public MagnitudeVector3d() // error: must assign 'X', 'Y', 'Z' before returning
    {
    }
}

確定割り当ての調整

明確な割り当て分析を実行して、this の未割り当てフィールドにエラーを与える代わりに、暗黙的に初期化する必要があるフィールドを決定するためにこれを実行します。 このような初期化は、コンストラクターの の先頭に挿入されます。

struct S
{
    int x, y;

    // Example 1
    public S()
    {
        // ok. Compiler inserts an assignment of `this = default`.
    }

    // Example 2
    public S()
    {
        // ok. Compiler inserts an assignment of `y = default`.
        x = 1;
    }

    // Example 3
    public S()
    {
        // valid since C# 1.0. Compiler inserts no implicit assignments.
        x = 1;
        y = 2;
    }

    // Example 4
    public S(bool b)
    {
        // ok. Compiler inserts assignment of `this = default`.
        if (b)
            x = 1;
        else
            y = 2;
    }

    // Example 5
    void M() { }
    public S(bool b)
    {
        // ok. Compiler inserts assignment of `y = default`.
        x = 1;
        if (b)
            M();

        y = 2;
    }
}

例 (4) と (5) では、結果の codegen にフィールドの "二重代入" が含まれる場合があります。 これは通常問題ありませんが、このような二重割り当てに懸念があるユーザーの場合には、以前は明確な割り当てエラー診断として使用されていたものを、既定で無効化されている警告診断として出力できます。

struct S
{
    int x;
    public S() // warning: 'S.x' is implicitly initialized to 'default'.
    {
    }
}

この診断の重大度を "error" に設定したユーザーは、C# 11 より前の動作にオプトインします。 このようなユーザーは、基本的に、手動で実装されたセッターを使用して、半自動プロパティを "シャットダウン" します。

struct S
{
    public int X
    {
        get => field;
        set => field = field < value ? value : field;
    }

    public S() // error: backing field of 'S.X' is implicitly initialized to 'default'.
    {
        X = 1;
    }
}

一見すると、これは機能の「穴」のように感じますが、実際には正しいことです。 診断を有効にすると、ユーザーは、コンパイラがコンストラクター内のフィールドを暗黙的に初期化したくないと言っています。 ここでは暗黙的な初期化を回避する方法がないため、手動でフィールドを宣言して割り当てたり、フィールド初期化子を含めたりするなど、手動で実装されたセッターとは異なる方法でフィールドを初期化する方法を使用します。

現在、JIT は ref を使用してデッド ストアを排除しません。つまり、これらの暗黙的な初期化には実際のコストが発生します。 しかし、それは修正可能かもしれません。 https://github.com/dotnet/runtime/issues/13727

インスタンス全体ではなく個々のフィールドを初期化することは、実際には単なる最適化である点に注目してください。 コンパイラは、どの戻り点においても確実に割り当てられていないフィールドや、thisの非フィールドメンバーアクセスの前に暗黙的に初期化されるフィールドに関する不変条件を満たす限り、任意のヒューリスティックを自由に実装できるべきです。

たとえば、構造体に 100 個のフィールドがあり、そのうちの 1 つだけが明示的に初期化されている場合は、99 個の他のフィールドの initobj を暗黙的に出力するよりも、全体に対して initobj を行う方が理にかなっている可能性があります。 ただし、他の 99 個のフィールドの initobj を暗黙的に出力する実装は引き続き有効です。

言語仕様の変更

標準の次のセクションを調整します。

https://github.com/dotnet/csharpstandard/blob/draft-v8/standard/expressions.md#12814-this-access

コンストラクター宣言にコンストラクター初期化子がない場合、this 変数は構造体型の out パラメーターとまったく同じように動作します。 特に、これは、インスタンス コンストラクターのすべての実行パスで変数が確実に割り当てられることを意味します。

この言語を次のように調整します。

コンストラクター宣言にコンストラクター初期化子がない場合、this 変数は構造体型の out パラメーターと同様に動作しますが、明確な代入要件 (§9.4.1) が満たされていない場合はエラーにならない点が異なります。 代わりに、次の動作を紹介します。

  1. this 変数自体が要件を満たしていない場合は、コンストラクター内の他のコードが実行される前に、this 内のすべての未割り当てインスタンス変数が、初期化 フェーズで既定値 (§9.3) に暗黙的に初期化されます。
  2. this 内のインスタンス変数 v が要件を満たしていない場合、または v 内の入れ子の任意のレベルのインスタンス変数が要件を満たしていない場合、v は、コンストラクター内の他のコードが実行される前に、初期化 フェーズの既定値に暗黙的に初期化されます。

デザイン会議

https://github.com/dotnet/csharplang/blob/main/meetings/2022/LDM-2022-02-14.md#definite-assignment-in-structs