19

これは少し時間がかかるので、ここにクイックバージョンがあります:

なぜこれがランタイムTypeLoadExceptionを引き起こすのですか?(そして、コンパイラーは私がそれをするのを防ぐべきですか?)

interface I
{
    void Foo<T>();
}

class C<T1>
{
    public void Foo<T2>() where T2 : T1 { }
}

class D : C<System.Object>, I { } 

Dをインスタンス化しようとすると、例外が発生します。


より長く、より探索的なバージョン:

検討:

interface I
{
    void Foo<T>();
}

class C<T1>
{
    public void Foo<T2>() where T2 : T1 { }
}

class some_other_class { }

class D : C<some_other_class>, I { } // compiler error CS0425

C.Foo()の型制約がの型制約と一致しないため、これは不正ですI.Foo()。コンパイラエラーCS0425を生成します。

しかし、私はルールを破ることができるかもしれないと思いました:

class D : C<System.Object>, I { } // yep, it compiles

ObjectT2の制約として使用することにより、その制約を否定しています。D.Foo<T>()すべてがから派生しているため、任意のタイプをに安全に渡すことができObjectます。

それでも、コンパイラエラーが発生することを期待していました。C#言語の意味では、「C.Foo()の制約はI.Foo()の制約と一致する必要がある」という規則に違反しており、コンパイラーは規則に固執するだろうと思いました。しかし、それはコンパイルします。コンパイラーは私がしていることを見て、それが安全であることを理解し、目をつぶっているようです。

私はそれでうまくいったと思ったが、ランタイムはそれほど速くないと言っている。のインスタンスを作成しようとするとD、TypeLoadExceptionが発生します。「タイプ'D'のメソッド'C`1.Foo'は、より弱いタイプパラメータ制約を持つインターフェイスメソッドを暗黙的に実装しようとしました。」

しかし、そのエラーは技術的に間違っていませんか?の制約を無効にするObjectためにを使用しないので、-と同等になります-より強くはありませんか?コンパイラは同意しているようですが、ランタイムは同意していません。C<T1>C.Foo()I.Foo()

私の主張を証明するために、私Dは方程式からそれを取り除くことによってそれを単純化しました:

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class some_other_class { }

class C : I<some_other_class> // compiler error CS0425
{
    public void Foo<T>() { }
}

だが:

class C : I<Object> // compiles
{
    public void Foo<T>() { }
}

これはコンパイルされ、に渡されるすべてのタイプに対して完全に実行されFoo<T>()ます。

なんで?ランタイムにバグがありますか、または(より可能性が高いですが)私が表示されていないこの例外の理由があります-その場合、コンパイラは私を止めるべきではありませんか?

興味深いことに、制約をクラスからインターフェースに移動することによってシナリオが逆転した場合...

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class C
{
    public void Foo<T>() { }
}

class some_other_class { }

class D : C, I<some_other_class> { } // compiler error CS0425, as expected

そして再び私は制約を否定します:

class D : C, I<System.Object> { } // compiles

今回は問題なく動作します!

D d := new D();
d.Foo<Int32>();
d.Foo<String>();
d.Foo<Enum>();
d.Foo<IAppDomainSetup>();
d.Foo<InvalidCastException>();

何でもあり、それは私には完全に理にかなっています。D(方程式の有無にかかわらず同じ)

では、なぜ最初の方法が壊れるのでしょうか?

補遺:

TypeLoadExceptionの簡単な回避策があることを追加するのを忘れました:

interface I
{
    void Foo<T>();
}

class C<T1>
{
    public void Foo<T2>() where T2 : T1 { }
}

class D : C<Object>, I 
{
    void I.Foo<T>() 
    {
        Foo<T>();
    }
}

明示的に実装することI.Foo()は問題ありません。暗黙的な実装のみがTypeLoadExceptionを引き起こします。今、私はこれを行うことができます:

        I d = new D();
        d.Foo<any_type_i_like>();

しかし、それはまだ特別な場合です。System.Object以外のものを使用してみてください。これはコンパイルされません。意図的にこのように機能するかどうかわからないので、これを行うと少し汚く感じます。

4

4 に答える 4

4

これはバグです。ジェネリックインターフェイスからジェネリックメソッドを実装すると、TypeLoadExceptionが発生し、ジェネリックインターフェイスで検証できないコードが発生し、タイプパラメータ制約でジェネリックメソッドが発生するを参照してください。ただし、それがC#のバグなのかCLRのバグなのかはわかりません。

[OPによって追加されました:]

あなたがリンクした2番目のスレッドでMicrosoftが言っていることは次のとおりです(私の強調):

あるセットの制約が別のセットと同じくらい強いかどうかを判断するために、ランタイムとC#コンパイラで使用されるアルゴリズムの間に不一致があります。この不一致により、C#コンパイラはランタイムが拒否するいくつかの構造を受け入れ、その結果、表示されるTypeLoadExceptionが発生します。このコードがその問題の兆候であるかどうかを判断するために調査中です。とにかく、コンパイラがこのようなコードを受け入れて実行時例外が発生するのは、確かに「設計による」ものではありません。

よろしく、

Ed Maurer C#コンパイラ開発リード

私が太字にした部分から、彼はこれがコンパイラのバグだと言っていると思います。それは2007年にさかのぼります。彼らがそれを修正することを優先するほど深刻ではないと思います。

于 2011-05-20T20:34:17.397 に答える
3

唯一の説明は、制約がメソッド宣言の一部であると見なされることです。そのため、最初のケースではコンパイラエラーです。

使用時にコンパイラがエラーを受け取らないobject...まあ、それはコンパイラのバグです

他の「制約」には、一般的な制約と同じプロパティがあります。

interface I
{
    object M();
}

class C
{
    public some_type M() { return null; }
}

class D : C, I
{
}

私は尋ねることができます:なぜこれが機能しないのですか?

分かりますか?これはあなたとまったく同じ質問です。objectで実装することは完全に有効ですsome_typeが、ランタイムもコンパイラもそれを受け入れません。

MSILコードを生成しようとして、私の例の実装を強制すると、ランタイムは文句を言います。

于 2011-05-16T02:28:27.960 に答える
3

暗黙的なインターフェースの実装には、メソッド宣言のジェネリック制約が同等であるという要件がありますが、コード内で必ずしも完全に同じである必要はありません。さらに、ジェネリック型パラメーターには、「where T:object」という暗黙の制約があります。そのためC<Object>、コンパイルを指定すると、制約がインターフェイスの暗黙的な制約と同等になります。(C#言語仕様のセクション13.4.3 )。

また、制約付きメソッドを呼び出す明示的なインターフェイス実装を使用すると機能することも正しいです。これは、インターフェイスメソッドから、制約が異ならないクラスの実装への非常に明確なマッピングを提供し、次に、同様の名前のジェネリックメソッド(インターフェイスとは関係のないメソッド)の呼び出しに進みます。その時点で、セカンダリメソッドの制約は、インターフェイスの解決の問題なしに、ジェネリックメソッドの呼び出しと同じ方法で解決できます。

2番目の例では、クラスからインターフェイスに制約を移動する方が適切です。これは、クラスがデフォルトでインターフェイスから制約を取得するためです。これは、該当する場合はクラス実装で制約を指定する必要があることも意味します(Objectの場合は該当しません)。渡すI<string>とは、コードでその制約を直接指定できないことを意味します(文字列は封印されているため)。したがって、明示的なインターフェイス実装の一部であるか、両方の場所の制約と等しいジェネリック型である必要があります。

私の知る限り、ランタイムとコンパイラーは制約に対して別々の検証システムを使用しています。コンパイラはこのケースを許可しますが、ランタイムベリファイアはそれを好みません。なぜこれに問題があるのか​​よくわからないことを強調したいのですが、Tが設定されることに応じて、そのクラス定義の潜在的なインターフェース制約を満たさないことは嫌いだと思います。に。他の誰かがこれについて決定的な答えを持っているなら、それは素晴らしいことです。

于 2011-05-18T20:42:01.993 に答える
0

インターフェイスベースのスニペットへの応答:

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class C : I<string> // compiler error CS0425
{
    public void Foo<T>() { }
}

問題は、コンパイラが次のことを認識していることだと思います。

  1. C.Foo()で必要な型制約を宣言していません。
  2. タイプとして文字列を選択した場合、タイプは文字列から継承できないため、C.Foo()に有効なTはありません。

この作業を実際に確認するには、T1として継承できる実際のクラスを指定します。

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class C : I<MyClass>
{
    public void Foo<T>() where T : MyClass { }
}

public class MyClass
{
}

文字列型が特別に扱われていないことを示すには、上記のMyClass宣言にsealedキーワードを追加するだけで、Cの型制約として文字列とともにT1を文字列として指定した場合に、同じように失敗することがわかります。 Foo()。

public sealed class MyClass
{
}

これは、文字列が封印されており、制約の基礎を形成できないためです。

于 2011-05-19T03:04:05.970 に答える