web-dev-qa-db-ja.com

ジェネリック構造体として強く型付けされたGUID

私はすでに次のようなコードで同じバグを2回作成しています:

void Foo(Guid appId, Guid accountId, Guid paymentId, Guid whateverId)
{
...
}

Guid appId = ....;
Guid accountId = ...;
Guid paymentId = ...;
Guid whateverId =....;

//BUG - parameters are swapped - but compiler compiles it
Foo(appId, paymentId, accountId, whateverId);

OK、これらのバグを防ぎたいので、強く型付けされたGUIDを作成しました。

[ImmutableObject(true)]
public struct AppId
{
    private readonly Guid _value;

    public AppId(string value)
    {            
        var val = Guid.Parse(value);
        CheckValue(val);
        _value = val;
    }      

    public AppId(Guid value)
    {
        CheckValue(value);
        _value = value;           
    }

    private static void CheckValue(Guid value)
    {
        if(value == Guid.Empty)
            throw new ArgumentException("Guid value cannot be empty", nameof(value));
    }

    public override string ToString()
    {
        return _value.ToString();
    }
}

また、PaymentIdのもう1つ:

[ImmutableObject(true)]
public struct PaymentId
{
    private readonly Guid _value;

    public PaymentId(string value)
    {            
        var val = Guid.Parse(value);
        CheckValue(val);
        _value = val;
    }      

    public PaymentId(Guid value)
    {
        CheckValue(value);
        _value = value;           
    }

    private static void CheckValue(Guid value)
    {
        if(value == Guid.Empty)
            throw new ArgumentException("Guid value cannot be empty", nameof(value));
    }

    public override string ToString()
    {
        return _value.ToString();
    }
}

これらの構造体はほとんど同じですが、コードの重複がたくさんあります。そうじゃない?

構造体の代わりにクラスを使用することを除いて、それを解決するエレガントな方法を見つけることはできません。 nullチェック、メモリフットプリントの削減、ガベージコレクタのオーバーヘッドがないなどの理由で、構造体を使用します。

コードを複製せずにstructを使用する方法はありますか?

39
Tomas Kubes

まず、これは本当に良いアイデアです。簡単な話:

C#を使用して、整数、文字列、IDなどの周りの安価な型付きラッパーを簡単に作成できるようにしたいと思います。私たちは、プログラマーとして非常に「ストリングハッピー」および「整数ハッピー」です。多くのことが文字列と整数として表され、型システムでより多くの情報を追跡できます。お客様の名前をお客様の住所に割り当てたくありません。しばらく前に、OCamlでの仮想マシンの作成に関する一連のブログ投稿を書きました(決して終わりません!)。私がやった最も良いことの1つは、その目的を示す型で仮想マシンのすべての整数をラップすることでした。それは非常に多くのバグを防ぎました! OCamlを使用すると、小さなラッパータイプを非常に簡単に作成できます。 C#はサポートしていません。

第二に、コードの複製についてあまり心配しません。ほとんどの場合、簡単なコピーアンドペーストであり、コードを大幅に編集したり、ミスをしたりすることはほとんどありません。 実際の問​​題を解決するために時間を費やしてください。少しコピーアンドペーストしたコードは大したことではありません。

コピー&ペーストされたコードを避けたい場合は、次のようなジェネリックを使用することをお勧めします。

struct App {}
struct Payment {}

public struct Id<T>
{
    private readonly Guid _value;
    public Id(string value)
    {            
        var val = Guid.Parse(value);
        CheckValue(val);
        _value = val;
    }

    public Id(Guid value)
    {
        CheckValue(value);
        _value = value;           
    }

    private static void CheckValue(Guid value)
    {
        if(value == Guid.Empty)
            throw new ArgumentException("Guid value cannot be empty", nameof(value));
    }

    public override string ToString()
    {
        return _value.ToString();
    }
}

これで完了です。 AppIdPaymentIdの代わりにタイプId<App>Id<Payment>がありますが、Id<App>Id<Payment>またはGuidに割り当てることはできません。

また、AppIdPaymentIdを使用したい場合は、ファイルの先頭で次のように言うことができます。

using AppId = MyNamespace.Whatever.Id<MyNamespace.Whatever.App>

等々。

第三に、おそらくあなたのタイプにはさらにいくつかの機能が必要でしょう。これはまだ完了していないと思います。たとえば、2つのIDが同じであるかどうかを確認できるように、おそらく平等が必要になります。

第4に、default(Id<App>)は依然として「空のGUID」識別子を提供するため、それを防止しようとしても実際には機能しないことに注意してください。作成することは引き続き可能です。その周りに本当に良い方法はありません。

45
Eric Lippert

私たちも同じことをします。

はい、大量のコピーアンドペーストですが、それがまさにコード生成の目的です。

Visual Studioでは、このために T4 テンプレートを使用できます。基本的にクラスを1回作成すると、「このクラスをApp、Payment、Account、...にしたい」というテンプレートがあり、Visual Studioがそれぞれに1つのソースコードファイルを生成します。

そうすれば、1つのソース(T4テンプレート)があり、クラスでバグを見つけた場合に変更を加えることができ、すべての識別子を変更することなく考えることができます。

5
nvoigt