インターフェースと言えばジェネリックの花形で、その定義から実装、利用まで含めて様々なパターンがあります。
しかし、ジェネリックを解説しているサイトなどでは大体が「抽象的無意味な例」しか載せていないように思えます。
そこで、かなり単純化した例ではありますが、一応そこそこ具体的な意味を持った例を考えてみました。
#ジェネリックインターフェースの例
とりあえず、ジェネリックインターフェースを定義してみます。
この頁で使うインターフェースはこれだけです。
public interface IGenerator<T>
{
T Generate();
}
型Tの変数を生み出し続けるインターフェースです。
IEnumerable<T>と似ていますが、無限に生み出すところが違います。
#普通の実装例
型パラメータTをintとして実装した例です。
public class NumberMachine : IGenerator<int>
{
int i = 0;
public int Generate()
{
return ++i;
}
}
整数を無限に生成します。
#型パラメータがさらにジェネリックな実装例
<>の中にそのまた<>があるやつです。
public class NumberMachineGenerator : IGenerator<IGenerator<int>>
{
public IGenerator<int> Generate()
{
return new NumberMachine();
}
}
上で定義したNumberMachineを無限に生成します。
適当な利用例を載せておきます。
var nm_generator = new NumberMachineGenerator();
var generators = new List<IGenerator<int>>();
for(int i = 0; i < 5; i++){
generators.Add(nm_generator.Generate());
}
int j = 1;
foreach(var g in generators){
for(int i = 0; i < j; i++){
Console.WriteLine(g.Generate());
}
j++;
}
ここで重要なのは、NumberMachineGeneratorがGenerateするのはあくまでIGenerator<int>であり、NumberMachineとしては認識されないということです。
後でその違いを説明します。
#ジェネリックのままでの実装例
型パラメータのTを保ったまま実装する例です。
public class NewGenerator<T> : IGenerator<T>
where T : new()
{
public T Generate()
{
return new T();
}
}
今回は型制約も付けました。引数無しのコンストラクタを持つTに適用でき、そのコンストラクタを使ってTを生み出し続けるクラスです。
#型制約に型パラメータを使用する実装例
型パラメータが二つであり、片方のパラメータがもう片方のパラメータの制約に使われる例です。
public class GeneratorGenerator<T, U> : IGenerator<T>
where T : IGenerator<U>, new()
{
public T Generate()
{
return new T();
}
}
IGeneratorを無限に生み出すIGeneratorです。つまり、このクラス自体がIGenerator<IGenerator<U>>を持っていることになります。
簡単な利用例を載せたいのですが、ちょうど最初に実装したNumberMachineが使えそうです。
public class NumberMachine : IGenerator<int>
{
int i = 0;
public int Generate()
{
return ++i;
}
}
デフォルトコンストラクタがnew()制約を満たします。
var gg = new GeneratorGenerator<NumberMachine, int>();
var generators = new List<NumberMachine>();
for(int i = 0; i < 5; i++){
generators.Add(gg.Generate());
}
int j = 1;
foreach(var g in generators){
for(int i = 0; i < j; i++){
Console.WriteLine(g.Generate());
}
j++;
}
最初の利用例と同じ結果になるので、確認してください。
ここでポイントとなるのは、今回はGeneratorGeneratorによってGenerateされるのがちゃんとNumberMachineであると認識されているところです。したがって、NumberMachine独自のメソッドなどがあればそれも使えます。最初の利用例では、それは不可能です。
##型パラメータを簡潔に書けないか?
一見、次のようにできそうです。
public class GeneratorGenerator<U> : IGenerator<IGenerator<U>>
{
public IGenerator<U> Generate()
{
return new IGenerator<U>(); //成立しない
}
}
しかし、これはnew IGenerator<U>()という命令が成立しないためコンパイルエラーですね。
#複数の型パラメータに制約が付く実装例
もはやジェネリックインターフェースが関係なくなってきましたが、いちおう例を載せておきます。
public class NewGeneratorGenerator<T, U> : IGenerator<T>
where T : NewGenerator<U>, new()
where U : new()
{
public T Generate()
{
return new T();
}
}
上で定義したNewGeneratorクラス(またはその派生クラス)を無限に生み出すGeneratorです。やはりこのクラスもIGenerator<IGenerator<U>>を持つことになります。
Uに型制約new()が必要な理由は、NewGeneratorの型パラメータとなるための型制約を満たさなければならないからです。
##型パラメータを簡潔に書けないか?
これも一見、次のようにできそうです。
public class GeneratorGenerator<U> : IGenerator<IGenerator<U>>
where U : new()
{
public NewGenerator<U> Generate()
{
return new NewGenerator<U>(); //成立はしている
}
}
これは一応成立はしています。しかし、Generateの戻り値がNewGenerator<U>になってしまいます。
元々の実装例では、Generateの戻り値はTです。つまり派生クラスもちゃんと派生クラスで認識されることになります。
#型パラメータの共変性
Ver4.0で追加された、型パラメータの共変性・反変性についての例です。
IGeneratorはTが戻り値だけに使われているので、共変性を設定できます。
public interface IGenerator<out T>
{
T Generate();
}
共変性はTが参照型の場合のみ使えるので、適当なクラスと継承関係を定義します。
public class Human
{
public string name;
}
public class Worker : Human
{
public string job;
}
IGenerator<Worker>を適当に実装します。
public class JohnGenerator:IGenerator<Worker>
{
public Worker Generate()
{
return new Worker { name = "John", job = "musician" };
}
}
ミュージシャンのJohnを無限に生み出すクラスです。
やっと共変性が利用できます。
IGenerator<Human> jg = new JohnGenerator(); //outを付けていないとここでエラー
Console.WriteLine(jg.Generate().name);
もしIGeneratorの定義でTにoutを付けていなければ、最初の行で「JohnGeneratorをIGenerator<Human>に暗黙的に変換できません」と怒られてしまいます。
共変性のおかげで、型パラメータが派生クラスから基底クラスになる変換が認められます。