Javaに似ているが、違う部分もある。
public class Hoge<T>
where T : Hoge<T>
{
public virtual void DoHoge(T hoge)
{
}
public void DoLoop(IList<T> list)
{
foreach (T hoge in list)
{
hoge.DoHoge(hoge);
}
}
}
public class Piyo : Hoge<Piyo>
{
public override void DoHoge(Piyo hoge)
{
}
}
public class Fuga : Piyo
{
public override void DoHoge(Piyo hoge)
{
}
}
public class Piyo2<P> : Hoge<P>
where P : Piyo2<P>
{
public override void DoHoge(P hoge)
{
}
}
public class Fuga2 : Piyo2<Fuga2>
{
public override void DoHoge(Fuga2 hoge)
{
}
}
public class Foo<X>
where X : Hoge<X>
{
}
同じに見えるかもしれないが、何が違うのか。
まず、.NETではHoge
型とHoge<T>
型は別の型だ。
// Hoge hoge = null; // NG
次に、型引数を指定していないジェネリック型(Javaではraw型と言うようだが、.NETでは「オープン型」と呼ぶ)は
Hoge<>
と書くのだが、これはローカル変数やメンバの型として使えない。
Type hogeType = typeof(Hoge<>); // Javaで言うところのHoge.classはOK
// Hoge<> hoge = null; // NG
型消去されるJavaが特別なんだいう見方もある。
そして、.NETのジェネリクスにはワイルドカードもない。
// Hoge<?> hoge = null; // NG
.NETでは、たとえばHoge<int>
型とHoge<string>
型は別の型であって、互換性はないからこれも当たり前といえば当たり前。
というわけで、次のコードは
Foo<Piyo> piyo; // OK
// Foo<Fuga> fuga; // NG
// Foo<Piyo2> piyo2; // NG
Foo<Fuga2> fuga2; // OK
Javaと同じに見えるかもしれないが、Foo<Piyo2>
がコンパイルできないのはJavaとは理由が異なっていて、
Piyo2
型がオープン型のため、Foo<Piyo2>
型もオープン型だから、変数の型として宣言できない。
ワイルドカードがないから、「境界をゆるめる」こともできない。
public class EvilPiyo : Hoge<Piyo>
{
public override void DoHoge(Piyo hoge)
{
}
}
このような型を定義することはできるが、以下のようなコードは依然として書けない。
// Foo<EvilPiyo> evilPiyo = null; // NG
他に方法はないのかというと、実際にはある。それが、ジェネリクスに変性を指定することだ。
実際、「境界を厳密にする」でやっている、<? super X>
のような書き方は、型引数に反変性を指定していることに他ならない。
ただし、.NETのジェネリクスの場合、以下の2点がJavaと異なっている。
- 型の定義側で変性を指定する。(Javaの場合は、型の利用側で指定する。)
- インターフェースまたはデリゲートでしかサポートされておらず、クラスや構造体では使えない。
ちなみにScalaだともっと柔軟だ。このあたりのことは、kmizuさんのブログ記事に詳しい。
で、そのような制限を踏まえて書き直すと、C#ではこうなる。
public interface IHoge<in T>
where T : IHoge<T>
{
void DoHoge(T hoge);
}
public class Piyo : IHoge<Piyo>
{
public virtual void DoHoge(Piyo hoge)
{
}
}
public class Fuga : Piyo
{
public override void DoHoge(Piyo hoge)
{
}
}
public class Piyo2<P> : IHoge<P>
where P : Piyo2<P>
{
public virtual void DoHoge(P hoge)
{
}
}
public class Fuga2 : Piyo2<Fuga2>
{
public override void DoHoge(Fuga2 hoge)
{
}
}
public class Foo<X>
where X : IHoge<X>
{
}
public class EvilPiyo : IHoge<Piyo>
{
public void DoHoge(Piyo hoge)
{
}
}
こんな風に定義すれば、めでたくこうなる。
Foo<Piyo> piyo; // OK
Foo<Fuga> fuga; // OK
// Foo<Piyo2> piyo2; // NG
Foo<Fuga2> fuga2; // OK
// Foo<EvilPiyo> evilPiyo; // NG
元記事に書かれていた
そんなわけで、変数宣言での
Foo<Piyo2<Piyo2<Piyo2<...>>>>
の極限みたいなのを今のJavaのジェネリクスでは表現できない。一旦継承した型を作る必要がある。
については.NETでも同様である。
(追記)こういう再起定義な型が関数型インターフェースで欲しくなる例としてYコンビネータ を挙げておきます。