44

次のように、共変型パラメーターを持つジェネリック インターフェイスがあるとします。

interface IGeneric<out T>
{
    string GetName();
}

そして、このクラス階層を定義すると:

class Base {}
class Derived1 : Base{}
class Derived2 : Base{}

次に、明示的なインターフェイス実装を使用して、次のように 1 つのクラスにインターフェイスを 2 回実装できます。

class DoubleDown: IGeneric<Derived1>, IGeneric<Derived2>
{
   string IGeneric<Derived1>.GetName()
   {
     return "Derived1";
   }

   string IGeneric<Derived2>.GetName()
   {
     return "Derived2";
   }  
}

(非ジェネリック)DoubleDownクラスを使用してキャストした場合、IGeneric<Derived1>またはIGeneric<Derived2>期待どおりに機能する場合:

var x = new DoubleDown();
IGeneric<Derived1> id1 = x;        //cast to IGeneric<Derived1>
Console.WriteLine(id1.GetName());  //Derived1
IGeneric<Derived2> id2 = x;        //cast to IGeneric<Derived2>
Console.WriteLine(id2.GetName());  //Derived2

ただし、 を にキャストするxIGeneric<Base>、次の結果が得られます。

IGeneric<Base> b = x;
Console.WriteLine(b.GetName());   //Derived1

呼び出しが 2 つの実装の間であいまいであるため、コンパイラがエラーを発行することを期待していましたが、最初に宣言されたインターフェイスが返されました。

なぜこれが許可されているのですか?

( 2つの異なるIObservableを実装するクラスに触発されましたか?同僚にこれが失敗することを示そうとしましたが、どういうわけか失敗しませんでした)

4

5 に答える 5

27

次の両方をテストした場合:

class DoubleDown: IGeneric<Derived1>, IGeneric<Derived2> {
    string IGeneric<Derived1>.GetName() {
        return "Derived1";
    }

    string IGeneric<Derived2>.GetName() {
        return "Derived2";
    }
}

class DoubleDown: IGeneric<Derived2>, IGeneric<Derived1> {
    string IGeneric<Derived1>.GetName() {
        return "Derived1";
    }

    string IGeneric<Derived2>.GetName() {
        return "Derived2";
    }
}

実際には、実装するインターフェイスを宣言する順序によって結果が変わることに気付いたに違いありません。しかし、私はそれが指定されていないだけだと思います。

まず、仕様 (§13.4.4 インターフェイス マッピング) には次のように記載されています。

  • 複数のメンバーが一致する場合、どのメンバーが IM の実装であるかは不明です
  • この状況は、S が、ジェネリック型で宣言された 2 つのメンバーのシグネチャが異なる構築型である場合にのみ発生しますが、型引数によってそれらのシグネチャが同一になります。

ここで、考慮すべき 2 つの質問があります。

  • Q1: ジェネリック インターフェイスには異なるシグネチャがありますか?
    A1: はい。IGeneric<Derived2>とですIGeneric<Derived1>

  • Q2: ステートメントIGeneric<Base> b=x;は、署名を型引数と同一にすることができますか?
    A2: いいえ。一般的な共変インターフェイス定義を介してメソッドを呼び出しました。

したがって、あなたの呼び出しは不特定の条件を満たしています。しかし、どうしてこれが起こるのでしょうか?

typeのオブジェクトを参照するために指定したインターフェースが何であれDoubleDown、それは常にDoubleDown. つまり、常にこの 2 つのGetName方法があります。それを参照するように指定したインターフェイスは、実際にはコントラクトの選択を実行します。

以下は、実際のテストからキャプチャされた画像の一部です

ここに画像の説明を入力

GetMembersこの画像は、実行時に何が返されるかを示しています。あなたがそれを参照するすべての場合において、IGeneric<Derived1>IGeneric<Derived2>またはIGeneric<Base>は違いはありません。次の 2 つの画像は、詳細を示しています。

ここに画像の説明を入力 ここに画像の説明を入力

示されている画像のように、これら 2 つのジェネリック派生インターフェイスには同じ名前がなく、別の署名/トークンによって同一になっています。

于 2013-02-05T17:05:22.823 に答える
25

コンパイラは行にエラーをスローできません

IGeneric<Base> b = x;
Console.WriteLine(b.GetName());   //Derived1

コンパイラが認識できるあいまいさがないためです。 GetName()実際、 interface の有効なメソッドですIGeneric<Base>bコンパイラは、あいまいさを引き起こす可能性のある型があることを知るために、実行時の型を追跡しません。したがって、何をすべきかを決定するのはランタイムに任されています。ランタイムは例外をスローする可能性がありますが、CLR の設計者はそれを拒否したようです (個人的には良い決断だったと思います)。

別の言い方をすれば、単にメソッドを書いただけだとしましょう:

public void CallIt(IGeneric<Base> b)
{
    string name = b.GetName();
}

IGeneric<T>アセンブリに実装するクラスを提供しません。あなたはこれを配布し、他の多くの人はこのインターフェースを一度だけ実装し、あなたのメソッドをうまく呼び出すことができます. ただし、最終的に誰かがアセンブリを使用してDoubleDownクラスを作成し、それをメソッドに渡します。コンパイラはどの時点でエラーをスローする必要がありますか? 確かに、への呼び出しを含む既にコンパイルおよび配布されたアセンブリはGetName()、コンパイラ エラーを生成できません。DoubleDownからへの代入IGeneric<Base>があいまいさを生み出していると言えます。しかし、もう一度、別のレベルの間接化を元のアセンブリに追加できます。

public void CallItOnDerived1(IGeneric<Derived1> b)
{
    return CallIt(b); //b will be cast to IGeneric<Base>
}

CallIt繰り返しになりますが、多くの消費者は or のいずれかを呼び出すことができ、CallItOnDerived1問題ありません。しかし、私たちの消費者の受け渡しDoubleDownも完全に合法的な呼び出しを行っており、からへCallItOnDerived1の変換は確かに問題ないはずなので、呼び出し時にコンパイラ エラーを引き起こすことはありません。したがって、コンパイラが の定義以外でエラーをスローできるポイントはありませんが、これにより、潜在的に有用な何かを回避策なしで実行する可能性が排除されます。DoubleDownIGeneric<Derived1>DoubleDown

私は実際にこの質問に別の場所でより詳細に回答しており、言語を変更できる場合の潜在的な解決策も提供しています。

反変性があいまいさをもたらす場合、警告やエラー (またはランタイム エラー) は発生しません。

これをサポートするために言語が変更される可能性は事実上ゼロであることを考えると、CLR のすべての実装が同じように動作することが期待されるように仕様に配置する必要があることを除いて、現在の動作は問題ないと思います。

于 2013-02-09T07:32:18.433 に答える
11

質問は、「なぜこれがコンパイラ警告を生成しないのですか?」と尋ねました。VBではそうです(私はそれを実装しました)。

型システムは、分散のあいまいさについての呼び出し時に警告を提供するのに十分な情報を持っていません。したがって、警告は早期に発行する必要があります...

  1. VBでは、とのC両方を実装するクラスを宣言するIEnumerable(Of Fish)IEnumerable(Of Dog)、一般的なケースでは2つが競合するという警告が表示されIEnumerable(Of Animal)ます。これは、完全にVBで記述されたコードからの差異のあいまいさを排除するのに十分です。

    ただし、問題のあるクラスがC#で宣言されている場合は役に立ちません。また、問題のあるメンバーを呼び出さない場合は、そのようなクラスを宣言するのが完全に合理的であることに注意してください。

  2. CVBでは、そのようなクラスからにキャストを実行すると、キャストIEnumerable(Of Animal)に警告が表示されます。これは、メタデータから問題のクラスをインポートした場合でも、分散のあいまいさを排除するのに十分です。

    ただし、実行可能ではないため、警告の場所としては適切ではありません。キャストを変更することはできません。人々への唯一の実行可能な警告は、戻ってクラス定義を変更することです。また、問題のあるメンバーを誰も呼び出さない場合は、そのようなキャストを実行することは完全に合理的であることに注意してください。

  • 質問:

    なぜVBはこれらの警告を発しますが、C#は発しませんか?

    答え:

    それらをVBに入れたとき、私は正式なコンピューターサイエンスに熱心で、コンパイラーを作成してから2年しか経っていませんでした。そして、それらをコーディングする時間と熱意がありました。

    Eric LippertはC#でそれらを実行していました。彼は、コンパイラーでそのような警告をコーディングするのに多くの時間がかかり、他の場所でよりよく費やすことができ、リスクが高いほど複雑であるということを理解するための知恵と成熟度を持っていました。実際、VBコンパイラには、VS2012でのみ修正されたこれらの警告にバグがありました。

また、率直に言って、人々が理解できるほど有用な警告メッセージを思いつくことは不可能でした。ちなみに、

  • 質問:

    呼び出すものを選択するときに、CLRはどのようにあいまいさを解決しますか?

    答え:

    これは、元のソースコードの継承ステートメントの辞書式順序、つまり、とを実装することを宣言した辞書式順序に基づいていCます。IEnumerable(Of Fish)IEnumerable(Of Dog)

于 2013-03-13T19:02:37.093 に答える
11

神聖な良さ、かなりトリッキーな質問に対するここでの本当に良い答えがたくさんあります。まとめ:

  • 言語仕様には、ここで何をすべきかが明確に記載されていません。
  • このシナリオは通常、誰かがインターフェースの共分散または反変性をエミュレートしようとしているときに発生します。C#にインターフェイスの差異があるので、このパターンを使用する人が少なくなることを願っています。
  • ほとんどの場合、「1つだけ選択する」のが妥当な動作です。
  • あいまいな共変変換で使用される実装をCLRが実際にどのように選択するかは、実装によって定義されます。基本的に、メタデータテーブルをスキャンして最初に一致するものを選択し、C#はたまたまソースコード順にテーブルを出力します。ただし、この動作に依存することはできません。どちらも予告なしに変更される場合があります。

もう1つ追加するだけです。つまり、悪いニュースは、この種のあいまいさが発生するシナリオでは、インターフェイスの再実装のセマンティクスがCLI仕様で指定された動作と正確に一致しないことです。幸いなことに、この種のあいまいなインターフェイスを再実装するときのCLRの実際の動作は、通常、必要な動作です。この事実を発見したことで、私、アンダース、および一部のCLI仕様メンテナの間で活発な議論が行われ、最終的には仕様または実装に変更はありませんでした。ほとんどのC#ユーザーは、最初にインターフェイスの再実装が何であるかさえ知らないため、これがユーザーに悪影響を与えないことを願っています。(これまでに私の注意を引いた顧客はいません。)

于 2013-03-13T21:18:23.190 に答える
2

「C# 言語仕様」を掘り下げてみると、動作が指定されていないようです (道に迷っていなければ)。

7.4.4 関数メンバーの呼び出し

関数メンバー呼び出しの実行時処理は、次の手順で構成されます。ここで、M は関数メンバーであり、M がインスタンス メンバーの場合、E はインスタンス式です。

[...]

o 呼び出す関数メンバーの実装が決定されます。

• E のコンパイル時の型がインターフェイスの場合、呼び出す関数メンバーは、E によって参照されるインスタンスの実行時の型によって提供される M の実装です。この関数メンバーは、インターフェイス マッピング規則を適用することによって決定されます(§ 13.4.4) E によって参照されるインスタンスの実行時型によって提供される M の実装を決定します。

13.4.4 インターフェースのマッピング

クラスまたは構造体 C のインターフェイス マッピングは、C の基本クラス リストで指定された各インターフェイスの各メンバーの実装を検索します。特定のインターフェイス メンバー IM の実装が決定されます。ここで、I はメンバー M が宣言されているインターフェイスです。各クラスまたは構造体 S を調べ、C から始めて、一致するものが見つかるまで、C の連続する各基本クラスに対して繰り返します。

• S に、I および M と一致する明示的なインターフェイス メンバーの実装の宣言が含まれている場合、このメンバーは IM の実装です。

• それ以外の場合、S に M に一致する非静的パブリック メンバーの宣言が含まれている場合、このメンバーは IM の実装です。複数のメンバーが一致する場合、どのメンバーが IM の実装であるかは指定されません。この状況は、S が、ジェネリック型で宣言された 2 つのメンバーのシグネチャが異なる構築型である場合にのみ発生しますが、型引数によってそれらのシグネチャが同一になります。

于 2013-01-28T13:26:10.157 に答える