読者です 読者をやめる 読者になる 読者になる

総称型の継承は難しい

.NET
public class B<T>
{
    public virtual void V(T t) {}
    public virtual void V(string t) {}
}

public class D : B<string> { } // Invalid

例えばこのように定義された型 D だが,CLI 仕様では無効らしい (ISO/IEC 23271:2006(E) §9.9).今のところ CLR で動作はするが.
また,過去に出荷された Microsoft 製 C# コンパイラは,上記コードを受け入れる.警告すら表示しない.(条件付きで) このコードに警告が与えられるようになるのは,次のコンパイラからだそうだ
ややこしいが,総称型のインスタンス化によってシグネチャが同じに見えてしまうこと自体は問題ないらしい(ISO/IEC 23271:2006(E) §9.8).

class public C<S,T>
{
    string f;
    S      f; // C# では同名のフィールドを許可しないが,規格上は可能
    void m(S x) {}
    void m(T x) {}
    void m(string x) {}
}

C<string, string> c; // Valid

一見,シグネチャが縮退してしまったように見えるが,CIL のレイヤでは MemberRef で一意に識別できる.だから問題ない,という理屈のようだ.

var a = c.(string C<string, string>::f);        // このような記法は C# には存在しない
var b = c.(!0 C<string, string>::f);            // このような記法は C# には存在しない
c.(void C<string, string>::m(!0))("hello");     // このような記法は C# には存在しない
c.(void C<string, string>::m(!1))("hello");     // このような記法は C# には存在しない
c.(void C<string, string>::m(string))("hello"); // このような記法は C# には存在しない

思考を整理するという意味でも,最初の例で何が Invalid だったのかを書いておこう.最初の例が Invalid になるべき理由は,(少なくとも私の理解の範囲内では),こうだ.
閉じた総称型 B<string> からクラス D を派生させるときに,そのまま何もしなければ何が起きるか? まずいのは,メソッド V のオーバーライドに関する曖昧さが発生することだ.C++ における有名な死のダイアモンドを思い出す.実は,この曖昧さを回避する方法がある.それは Explicit Override によるメソッドのリネーミングを行うことだ(ISO/IEC 23271:2006(E) §9.10).しかし最初の例ではそれを怠った.故に Invalid である.
次の C# コードを実行すると,type load exception が発生する.A::Foo および C::Foo が実装を持たないままクラス D がインスタンス化されるためだ.しかし,現行の Microsoft 製 C# コンパイラはこのコードをエラーとしない.コンパイラは勘違いしている.実際には,仮想メソッドの実装は不十分である.

using System;

abstract class A<T>
{
    public abstract void Foo(T x);
}

abstract class B<T, S> : A<T>
{
    public virtual void Foo(S x){}
}

abstract class C<T, S> : B<T, S>
{
    public abstract override void Foo(T x);
}

class D : C<int, int>
{
    public override void Foo(int x) {}

    static void Main()
    {
        new D().Foo(1);
    }
}

実は,コンパイラの勘違いが問題なのではない.勘違いの原因は,どのメソッドがオーバーライドされるのかに曖昧性が生じたことにある.規格は,そんな曖昧性自体を作るなと言っている.
繰り返すが,曖昧性を排除する方法はきちんと存在する.それはクラス D の定義時に仮想メソッドのリネーミングを行うことだ.これを Explicit Override という.
しかし不幸なことに,C# は完全な Explicit Override をサポートしない.C# に許されている Explicit Override は,インターフェイスの明示的な実装のみだ.
以下に,CLI 仕様が期待していると思われる回答例のひとつを示す.ただし私の理解の範囲内では,の話だが.
言語は C++/CLI.わかりやすさのためにクラス E も追加した.

generic<typename T>
ref class A abstract
{
public:
    virtual void Foo(T x) abstract;
};

generic<typename T, typename S>
ref class B abstract: public A<T> 
{
public:
    virtual void Foo(S x)
    {
        Console::WriteLine("B::Foo");
    }
};

generic<typename T, typename S>
ref class C abstract : public B<T, S>
{
public:
    virtual void Foo(T x) override abstract;
};

ref class D : public C<int, int>
{
public:
    virtual void Bar(int x) = C<int,int>::Foo
    {
        Console::WriteLine("D::Bar");
    }
};

ref class E : public D
{
public:
    virtual void Foo(int x) override
    {
        Console::WriteLine("E::Foo");
    }
};

仮想関数の系列は二種類だ.次のテーブルは,二種類の仮想関数がオーバーライドされていく様子を示している.

クラス メソッド系列1 メソッド系列2
A<int> Foo(!0)
B<int,int> Foo(!1)
C<int,int> Foo(!0)
D Bar(int)
E Foo(int)

実行例も示そう.

int main(array<System::String ^> ^args)
{
    {
        E e;
        e.Foo(1); // E::Foo(int)
        e.Bar(1); // D::Bar(int)

        D% d = e;
        d.Foo(1); // C<int,int>::Foo(!0)
        d.Bar(1); // D::Bar(int)

        C<int,int>% c = d;
        c.Foo(1); // C<int,int>::Foo(!0)

        B<int,int>% b = d;
        b.Foo(1); // B<int,int>:Foo(!1)
        
        A<int>% a = d;
        a.Foo(1); // A<int>::Foo(!0)
    }
    Console::WriteLine();
    {
        D d;
        d.Foo(1); // C<int,int>::Foo(!0)
        d.Bar(1); // D::Bar(int)

        C<int,int>% c = d;
        c.Foo(1); // C<int,int>::Foo(!0)

        B<int,int>% b = d;
        b.Foo(1); // B<int,int>:Foo(!1)
        
        A<int>% a = d;
        a.Foo(1); // A<int>::Foo(!0)
    }
}

/* 実行結果
E::Foo
D::Bar
D::Bar
D::Bar
D::Bar
E::Foo
D::Bar

D::Bar
D::Bar
D::Bar
B::Foo
D::Bar */

とここまで書いておいて何だが,今回の記事の正確性は非常に怪しい.内容を信じる前に必ず正式な仕様に目を通すようお願いしたい.仕様は誰でも無償で参照することが出来る.