[C#]ジェネリックなstructをインターフェースとして扱ったときのメモリアロケーションをどうしても避けたいときの小ネタ

C#は`struct`を使用するとスタック領域に確保されるのでヒープ領域からメモリアロケーションを避けることができます。

```csharp
public strct S
{
    public int v;
}

// ...

public static void Hoge()
{
    // ヒープからのアロケーションが発生しない
    var s = new S();
}
```

ただし、`struct`にインターフェースを実装したうえでそのインターフェースのオブジェクトとして扱うとメモリアロケーションが発生してしまいます。

```csharp
public interface IStore<T>
{
    T Value { get; }
}

public struct Store<T> : IStore<T>
{
    public T Value { get; set; }
}

// ...

// structをそのまま引数に取る
public static void HogeS<T>(Store<T> s)
{
    _ = s.Value;
}

// interfaceで受け取る
public static void HogeI<T>(IStore<T> s)
{
    _ = s.Value;
}

public static void Hoge()
{
    var s = new Store<int>();
    // アロケーションなし
    HogeS(s);
    // アロケーションあり!
    HogeI(s);
}
```

これを回避するためにはメソッドをジェネリックなものに変更する必要があります。

```csharp
// 引数をジェネリックなTStoreに変更
// TStoreにIStore<T>制約をかけるためにTも型引数とする必要がある
public static void HogeG<TStore, T>(TStore s)
    where TStore : IStore<T>
{}

public static void Hoge()
{
    var s = new Store<int>();
    // アロケーションなし
    HogeS(s);
    // アロケーションあり!
    HogeI(s);
    // アロケーションなし
    HogeG<IStore<int>, int>(s);
    
    // 型引数が推論できずにエラー
    // メソッド 'A.HogeG<TStore, T>(TStore)' の型引数を使い方から推論することはできません。型引数を明示的に指定してください。
    // HogeG(s);
}
```

メソッドをジェネリックなものに変更すると**型引数を明示的に指定**しなければならなくなります。
これは引数に`TStore`しか現れておらず`T`の型が分からないためです。
(理論的にはわかる気もしますがこの記事を書いている時点では無理でした。)

今回書いたサンプルでは型引数が`int`なのでまだ書けないこともないですが、以下のような場合は真面目に書いていられません。

```csharp
public static Store<T> Create<T>(T v) => new Store<T> { Value = v };

public static void Fuga()
{
    var s = Create((1, 2, 3u, (byte)4, Create((5f, 6.0, "7")), '8'));

    // !????
    HogeG<
        Store<(int, int, uint, byte, Store<(float, double, string)>, char)>,
        (int, int, uint, byte, Store<(float, double, string)>, char)
    >(s);
}
```

これは極端な例ですがすべてを`struct`で済ませつつアロケーションを回避するためにはこんなことになってしまいます。
できればこんな型引数は書きたくないものです。
こんなことになるのは引数から`T`を推論できないからなので**引数から推論できるようにする**と書かなくてもよくなります。
ただし`T`を引数に入れてしまうとスタックを多く消費する、そもそも`T`型の値をどうやって取得するのか、という問題があります。

```csharp
// Tを引数に取るとTのサイズ分のスタックの確保とコピーが発生してしまう
// そもそもTの値をどうやって取得するのかという問題がある
public static void HogeG<TStore, T>(TStore s, T __)
    where TStore : IStore<T>
{
    _ = s.Value;
}
```

そういう場合は型情報を提供するためだけの`struct`を用意します。

```csharp
public struct TypeHint<T>
{
    public Type Type => typeof(T);
    public T Default => default;
}
```

これを引数にします。

```csharp
// 第二引数にTypeHint<T>をとるが推論にのみ用いる
public static void HogeG<TStore, T>(TStore s, TypeHint<T> __ = default)
    where TStore : IStore<T>
{
    _ = s.Value;
}

// Store<T>からTypeHint<T>を取得するメソッド
public static TypeHint<T> GetTypeHint<T>(Store<T> _) => default;

public static void Fuga()
{
    var s = Create((1, 2, 3u, (byte)4, Create((5f, 6.0, "7")), '8'));

    // 型引数を書かなくてよい!!
    HogeG(s, GetTypeHint(s));

    // TypeHintを渡さないと推論できずにエラー...
    // HogeG(s);

    // ついでにIStore<T>.Valueの型のデフォルト値の変数を作ることもできる
    var v = GetTypeHint(s).Default;
}
```

複数の型パラメータを持つメソッドを作る場合も同様です。

```csharp
// 型情報を提供するstruct
public struct TypeHint<T1, T2>
{
    public TypeHint<T1> Arg1 => default;
    public TypeHint<T2> Arg2 => default;
}

// 二つの異なる型のIStoreを取る
public static void HogeG<TStore1, T1, TStore2, T2>(TStore1 store1, TStore2 store2, TypeHint<T1, T2> __ = default)
    where TStore1 : IStore<T1>
    where TStore2 : IStore<T2>
{
    _ = store1.Value;
    _ = store2.Value;
}

// 1型引数のTypeHintから2型引数のTypeHintを作る
public static TypeHint<T1, T2> CreateTypeHint<T1, T2>(TypeHint<T1> _, TypeHint<T2> __) => default;

public static void Piyo()
{
    var s1 = Create((1, 2, 3u, (byte)4, Create((5f, 6.0, "7")), '8'));
    var s2 = Create(((9, 10, "11"), new [] { 12, 13 }, (14u, 15f)));

    // 型引数を書かなくてよい!!
    HogeG(s1, s2, CreateTypeHint(GetTypeHint(s1), GetTypeHint(s2)));

    // こんなこともできる
    var s3 = Create((s1, s2));
    var s4 = Create((s1, s2, s3));
    HogeG(s3, s4, CreateTypeHint(GetTypeHint(s3), GetTypeHint(s4)));
}
```


ちなみに最後のメソッドの型引数は以下のようになります。
(スクロールがあるので全部見えていません。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42193/a84142c3-1fc5-057c-21c2-37aaa287b2b8.png)

## まとめ

おすすめはしません。
素直にインターフェースとして扱ったほうが便利だとおもいます。
ふと思いついたので記事にしました。:eyes: