次の方法で共有


自動デフォルト構造体

メモ

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

機能の仕様と行われた実装では、いくつかの違いがあることがあります。 これらの違いは、関連する言語設計ミーティング (LDM) メモに取り上げられています。

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

チャンピオンの課題: https://github.com/dotnet/csharplang/issues/5737

まとめ

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

目的

この提案は、dotnet/csharplang#5552 および dotnet/csharplang#5635 で見つかった使いやすさの問題の緩和策になるだけでなく、#5563 (すべてのフィールドは明確に代入される必要があるが、コンストラクター内で field にアクセスできない) の問題にも対応しています。


C# 1.0 以降、構造体コンストラクターでは、thisout パラメーターのように明確に代入することが必要になっています。

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) と比べて面倒になる可能性があります。 自動プロパティは「自動」であることが想定されており、おそらくそれにはフィールドの「自動」初期化も含まれます。 プロパティへの代入によって基になるフィールドがいつ代入されるのか、プロパティのセッターがいつ呼び出されるのかについて、混乱が生じる可能性があります。

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

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) では、生成されるコードでフィールドの「二重代入」が起こる場合があります。 これは通常問題ありませんが、このような二重割り当てに懸念があるユーザーの場合には、以前は明確な割り当てエラー診断として使用されていたものを、既定で無効化されている警告診断として出力できます。

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

この診断の重大度を「エラー」に設定しているユーザーは、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 は参照を介したデッド ストア (不要な代入) を排除しないため、これらの暗黙的な初期化には実際のコストが生じます。 しかし、この問題は修正できる可能性があります。 https://github.com/dotnet/runtime/issues/13727

注目すべき点として、インスタンス全体を一度に初期化せず個々のフィールドを個別に初期化する方法は、単なる最適化手法の 1 つに過ぎません。 コンパイラは、どの戻り点においても確実に割り当てられていないフィールドや、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