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)));
}
ちなみに最後のメソッドの型引数は以下のようになります。
(スクロールがあるので全部見えていません。)
まとめ
おすすめはしません。
素直にインターフェースとして扱ったほうが便利だとおもいます。
ふと思いついたので記事にしました。