ネコのために鐘は鳴る

寺院に住み着くパソコ〇好き

(C#) C#10 で構造体の既定のコンストラクタを禁止する

構造体の既定のコンストラク

構造体 (struct) には既定のコンストラクタが暗黙的に定義されます。

public struct MyStruct
{
    public string? Value { get; }

    public MyStruct(string? value)
    {
        Value = value;
    }
}

// 既定のコンストラクタ
var myStruct = new MyStruct();
// myStruct.Value == null
Console.WriteLine(myStruct.Value);

既定のコンストラクタはすべてのフィールドが0や null で初期化された状態であり、この引数のない既定のコンストラクタを自分で書くことはできませんでした。つまり、C#9.0 以前のコードでは default(T) と同義でした。

ところが、C#10.0 からは構造体に引数無しのコンストラクタを定義できるようになりました。ただし、互換性のため、引数無しのコンストラクタが定義されていない場合は引き続き default(T) と同じ挙動をする既定のコンストラクタが定義されます。

構造体の既定のコンストラクタを禁止する

ハイパフォーマンスな実装においてはクラスを避けて構造体で記述したいが、既定のコンストラクタでは正しい状態を保てない場合があります。そのため仕方なくクラスで記述したり、安全のためのチェックを書かざるを得ない場合があります。例えば、前述の MyStruct は既定のコンストラクタを使用した場合を考慮して Value プロパティを null 許容型の string? にせざるをえないなどの状況です。

C#9.0 以前ではこれを防ぐ方法はなく、Roslyn アナライザなど使わない限り使用者に注意喚起を表示することもできませんでした。 C#10.0 ではこれを明示的に禁止できます。

public struct MyStruct
{
    public string Value { get; }

    [Obsolete("Don't use default constructor", true)]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public MyStruct()
    {
        throw new NotSupportedException("Don't use default constructor");
    }

    public MyStruct(string value)
    {
        ArgumentNullException.ThrowIfNull(value);
        Value = value;
    }
}

Obsolete 属性をつけた引数無しのコンストラクタを記述します。Obsolete 属性の2つ目の引数を true にすると、使用者がこのコンストラクタを記述した場合にコンパイルエラーにすることができます。

また、[EditorBrowsable(EditorBrowsableState.Never)] をつけることで IDE などのインテリセンスに表示されなくなるため、使用者側からは存在していないように見えます (Visual Studio 2022 で確認済み)。ただし、[EditorBrowsable(EditorBrowsableState.Never)] の効果はパッケージや dll を参照した時にしか効果がないため、同一アセンブリ内やプロジェクトを参照している場合などには見えてしまいます。

default とジェネリクスは禁止できない

Obsolete 属性のコンパイルエラーの抜け道

Obsolete 属性によるコンパイルエラーの抜け道として、ジェネリクスwhere T : new() 制約によるインスタンス生成があります。

public static T GetInstance<T>() where T : new()
{
    return new T();
}

// コンパイルエラーにはならない
var myStruct = GetInstance<MyStruct>();

上記の場合、MyStruct() の中で NotSupportedException を投げているためインスタンス自体が生成されることはありませんが、コンパイルエラーとして静的に禁止することはできません。

default(T) の扱い

引数無しコンストラクタは禁止できますが、default および配列などの規定値としてゼロ初期化された構造体が使われてしまうことはあります。

var array = new MyStruct[10];
Console.WriteLine(array[0].Value);  // null

MyStruct myStruct = default;
Console.WriteLine(myStruct.Value);  // null

Roslyn アナライザを使わない限り、構造体の default を禁止することはできません (アナライザを使っても完璧に禁止するのは難しいです)。 構造体で default を書くことは、参照型において null と書くのと同義ととらえると、「変数を初期化していない (null) なのだから、その値を使うと正しく動かないのは当然」と考えることもできなくはないですが、C# の歴史的に見てやや無理があるのも否めません。

 

ゼロ初期化状態の構造体を不正なものとして既定のコンストラクタを禁止するのが効果的に機能する場面は、ハイパフォーマンスなチューニングを必要とする箇所であり、それを実装する人も使う人もある程度 C# に知識のある人です。default(T) の扱いは十分理解していると考えると問題ないのかもしれませんが、ライブラリとして広く一般に使う場面を想定するとやや無理があるのも理解できます。場面に応じて適切に使うべきでしょう。

なんでも構造体実装にするのは速い反面、このような問題もありますが、既定のコンストラクタを静的にある程度禁止できるのはかなり有効です。