LoginSignup
3
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-05-16

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

public strct S
{
    public int v;
}

// ...

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

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

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);
}

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

// 引数をジェネリックな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なのでまだ書けないこともないですが、以下のような場合は真面目に書いていられません。

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型の値をどうやって取得するのか、という問題があります。

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

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

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

これを引数にします。

// 第二引数に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;
}

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

// 型情報を提供する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

まとめ

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

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2