Skip to content

Instantly share code, notes, and snippets.

@matarillo
Last active August 29, 2015 14:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save matarillo/80b6a9148009651aa313 to your computer and use it in GitHub Desktop.
Save matarillo/80b6a9148009651aa313 to your computer and use it in GitHub Desktop.

#03 再帰的ジェネリクスの代入互換性 ... のC#版

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コンビネータ を挙げておきます。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment