てくメモ

trivial な notes

【C#】ジェネリック型引数でキャスト

ある型をジェネリック型引数の型にキャストしたいというとき、objectを経由するという手筋がある。

private readonly struct WithBox<T>
{
    public readonly double Value;
    public WithBox(double value) => Value = value;

    public T Convert()
    {
        if (typeof(T) == typeof(int))
        {
            // 以下は型制約抜きではコンパイルが通らない
            // return (T)Value;

            // object 経由だと成立する
            return (T)(object)(int)Value;
        }

        throw new InvalidOperationException();
    }
}

上記のようなキャストで、無事Tが返る。


ただ、今回のようにキャスト対象が値型だとボクシング、Tが値型ならさらにアンボクシングが入る。
これらに嫌な気持ちを持つ人も少なくないはず。


これらを避けるため、デリゲートを用いる手法がある。
参考:ジェネリックメソッドで値型を返す時にボックス化させない方法 - cactuaroid blog

private readonly struct WithoutBox<T>
{
    public readonly double Value;
    public WithoutBox(double value) => Value = value;

    public T Convert()
    {
        if(typeof(T) == typeof(int))
        {
            var func = (Func<double, T>)(object)Conv;

            // Value のボクシング・アンボクシングがない
            return func(Value);
        }

        throw new InvalidOperationException();
    }

    private static int Conv(double d) => (int)d;
}

なるほど! という感じ。


ただ、単純にキャストする場合と較べてデリゲートを使うという気にかかる点はある。
実際のところどれくらいの効能なのだろうか。気になったら試すが吉。

[Benchmark]
public int CastWithBox()
{
    WithBox<int> v = new(Math.PI);
    return v.Convert();
}

[Benchmark]
public int CastWithoutBox()
{
    WithoutBox<int> v = new(Math.PI);
    return v.Convert();
}
|         Method |      Mean |     Error |    StdDev |    Median | Allocated |
|--------------- |----------:|----------:|----------:|----------:|----------:|
|    CastWithBox | 0.0180 ns | 0.2963 ns | 0.0162 ns | 0.0222 ns |         - |
| CastWithoutBox | 6.3944 ns | 1.2450 ns | 0.0682 ns | 6.4225 ns |         - |


😥

Keep it simple への敗北。


なお、標準ライブラリもobject経由キャスト。
参考:Source Browser(リンク先double.TryConvertTo

なんだか悔しくてさらにUnsafe.As<int, T>利用も考えたけれど、object経由キャストで既に刹那な時間なのでここで終わり。