インターフェイス制約
C#のジェネリックでは、メソッドなどのメンバーを参照するためにインターフェイス制約(制約条件で、where T : [interface]
)を付ける必要があります。
例えば、以下のようなコードでは、前者の書き方ではだめで、
後者のように、where
句を付けて初めてコンパイルできます。
// コンパイル エラー: T に Count プロパティがない
static int M<T>(T x) => x.Count;
// これなら大丈夫。IList.Count を参照できる
static int M<T>(T x)
where T : System.Collections.IList
=> x.Count;
インターフェイス制約で困ることになるのは、静的メソッドを呼べないことです。 演算子も静的メソッドみたいなものなので呼べません。 例えば、以下のコードはコンパイル エラーになります。
// interface 制約では静的メソッドを呼べない
// なので、ジェネリックを使うと静的メソッドを呼ぶ手段がない
// コンパイル エラーに
static T M<T>(T x) => T.StaticMethod(x);
// + (演算子)は実質的には静的メソッド
// 演算子もコンパイル エラーに
static T Add<T>(T x, T y) => x + y;
インターフェイス制約が必要なんだったら、インターフェイスをそのまま使えばいいと思うかもしれませんが、 わざわざジェネリックにすることで実行性能的に有利になることがあります。
特に、構造体が絡むと顕著で、かなり実行性能に影響があります。 例えば以下のコードを見てください。
using System;
// 無駄なヒープ確保をしないように構造体に
struct Disposable : IDisposable
{
public void Dispose() { }
}
class Program
{
static void WithInterface(IDisposable x) => x.Dispose();
// やってることは WithInterface を同じに見えて…
static void WithGenerics<T>(T x)
where T : IDisposable
=> x.Dispose();
static void Main()
{
// 構造体なので無駄なヒープ確保はしない
default(Disposable).Dispose();
for (int i = 0; i < 10000; i++)
{
// ところが、インターフェイスを介するとボックス化を起こす
// 無駄なヒープ確保に
// 1個や2個なら大したコストではないものの、何度も呼ばれるとさすがにつらい
WithInterface(default(Disposable));
}
for (int i = 0; i < 10000; i++)
{
// ジェネリックを介するとボックス化が不要
// 繰り返し呼んでも平気
WithGenerics(default(Disposable));
}
}
}
IDisposable
インターフェイスを実装したDisposable
という構造体を作って、このDispose
メソッドを呼ぶことを考えます。
IDisposable
インターフェイスなのは簡単に実装できるものを選んだというだけで、深い意味はありません。
構造体なので、普通にインスタンスを作って、普通にDispose
メソッドを呼ぶ分にはヒープ領域を一切使いません。
ところが、WithInterface
メソッドのように、インターフェイスを介して引数に渡すと、ここでボックス化(ヒープ確保)が発生します。
1個や2個なら大したコストではないものの、この例のようにループの内側で大量に呼ばれると、なかなかきつい負担となります。
そこで、WithGenerics
メソッドのように、ジェネリックを使います。
前節で説明しましたが、
C#のジェネリックでは値型を使ったときにコードを展開してくれます。
その結果、ボックス化を起こさずにメソッドの引数に値型を渡せます。
静的メソッド代わり
ジェネリックでは静的メソッドを呼ぶ手段がないという話をしました。 ちょっと強引な手段にはなりますが、この代わりとなる方法を考えてみましょう。
たとえば、以下のような累算処理を考えてみます。
int
配列の全要素の和と積を求めるコードです。
static int Sum(int[] items)
{
var sum = 0;
foreach (var item in items)
sum = sum + item;
return sum;
}
static int Prod(int[] items)
{
var sum = 1;
foreach (var item in items)
sum = sum * item;
return sum;
}
static void M()
{
var items = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var sum = Sum(items);
var prod = Prod(items);
}
標準ライブラリにあるAggregate
メソッド(System.Linq.Enumerable
クラスの拡張メソッド)を使って以下のように書けはするんですが。
これはこれで、デリゲート(インターフェイスと同程度の負担)を介することになります。
var sum = items.Aggregate(0, (x, y) => x + y);
var prod = items.Aggregate(1, (x, y) => x * y);
デリゲートやインターフェイスを介したメソッド呼び出しは、静的メソッドと比べるとほんの少し不利です。 微々たるものですが、「ちりも積もれば」で、微々たる差を気にしないといけないこともあります。
そこで、値型のジェネリックが展開される性質を使ってみます。 まず、以下のようなインターフェイスと構造体を作ります。
interface IBinaryOperator<T>
{
T Zero { get; }
T Operate(T x, T y);
}
struct Add : IBinaryOperator<int>
{
public int Zero => 0;
public int Operate(int x, int y) => x + y;
}
struct Mul : IBinaryOperator<int>
{
public int Zero => 1;
public int Operate(int x, int y) => x * y;
}
例えば、以下のように書けます。 値型のジェネリックの展開によって、デリゲートやインターフェイスを介するよりも最適化が掛かりやすく、 静的メソッドに近い性能になります。 (具体的にいうと、仮想メソッド呼び出しが消えて、小さいメソッドを最適化オプション付きで実行するとインライン展開も掛かります。 この例はまさにそういう最適化が掛かって、ジェネリックなしの場合と比べて10倍以上速くなったりします。)
static T Sum<T, TOperator>(T[] items, TOperator op)
where TOperator : struct, IBinaryOperator<T>
{
var sum = op.Zero;
foreach (var item in items)
sum = op.Operate(sum, item);
return sum;
}
static void M()
{
var items = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// ジェネリックを介せばボックス化を避けれる
var sum = Sum(items, default(Add));
var prod = Sum(items, default(Mul));
}
もう1つ、違うバージョンを書いてみましょう。
default(Add)
とか、呼び出し側でダミーのインスタンスを作って引数として渡すのも無駄なので、
これもメソッドの中でやってしまいましょう。
static T Sum<T, TOperator>(T[] items)
where TOperator : struct, IBinaryOperator<T>
{
var sum = default(TOperator).Zero;
foreach (var item in items)
sum = default(TOperator).Operate(sum, item);
// ↑ メソッド内で default()
// 空の構造体なのでほぼノーコスト
return sum;
}
static void M()
{
var items = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
// default(T) せず、型引数だけ書く
var sum = Sum<int, Add>(items);
var prod = Sum<int, Mul>(items);
}
こういう、型引数の変更だけで動作を切り替える手法をポリシー パターン(policy pattern)とかポリシー ベース設計(policy based design)とか呼んだりします。 ちなみに、C++のtemplateの場合はジェネリックに静的メソッドを呼べるので、C++では強引な手段を取らなくてもポリシー パターンを使いやすく、結構多用されます。