43

まず、.NETはとのString両方IConvertibleであることを忘れないでICloneableください。

ここで、次の非常に単純なコードについて考えてみます。

//contravariance "in"
interface ICanEat<in T> where T : class
{
  void Eat(T food);
}

class HungryWolf : ICanEat<ICloneable>, ICanEat<IConvertible>
{
  public void Eat(IConvertible convertibleFood)
  {
    Console.WriteLine("This wolf ate your CONVERTIBLE object!");
  }

  public void Eat(ICloneable cloneableFood)
  {
    Console.WriteLine("This wolf ate your CLONEABLE object!");
  }
}

次に、次のことを試してください(いくつかの方法の中で):

ICanEat<string> wolf = new HungryWolf();
wolf.Eat("sheep");

これをコンパイルしても、コンパイラエラーや警告は表示されません。class実行すると、呼び出されるメソッドは、の宣言内のインターフェイスリストの順序に依存しているように見えますHungryWolf。(コンマ(,)で区切られたリストの2つのインターフェイスを交換してみてください。)

質問は単純です:これはコンパイル時の警告を与える(または実行時にスローする)べきではありませんか?

私はおそらくこのようなコードを思いついた最初の人ではありません。インターフェースの共変性を使用しましたが、インターフェースの共変性を使用して完全に類似した例を作成できます。そして実際、リッパート氏はずっと前にそれをしました。彼のブログのコメントでは、ほとんどの人がそれがエラーであるべきだということに同意しています。しかし、彼らはこれを黙って許可します。なんで?

---

拡張された質問:

上記では、aが(インターフェイス)と(インターフェイス)のString両方であることを利用しました。これらの2つのインターフェースはどちらも他方から派生していません。IconvertibleICloneable

これが、ある意味で少し悪い基本クラスの例です。

aは(直接基本クラス)と(基本クラスの基本クラス)のStackOverflowException両方であることに注意してください。次に(以前の場合):SystemExceptionExceptionICanEat<>

class Wolf2 : ICanEat<Exception>, ICanEat<SystemException>  // also try reversing the interface order here
{
  public void Eat(SystemException systemExceptionFood)
  {
    Console.WriteLine("This wolf ate your SYSTEM EXCEPTION object!");
  }

  public void Eat(Exception exceptionFood)
  {
    Console.WriteLine("This wolf ate your EXCEPTION object!");
  }
}

それをテストします:

static void Main()
{
  var w2 = new Wolf2();
  w2.Eat(new StackOverflowException());          // OK, one overload is more "specific" than the other

  ICanEat<StackOverflowException> w2Soe = w2;    // Contravariance
  w2Soe.Eat(new StackOverflowException());       // Depends on interface order in Wolf2
}

それでも警告、エラー、例外はありません。classそれでも、宣言のインターフェイスリストの順序に依存します。しかし、私がもっと悪いと思う理由は、今回は、過負荷SystemExceptionの解決が単なるよりも具体的であるため、常に選択するだろうと誰かが考えるかもしれないからExceptionです。


バウンティが開かれる前のステータス:2人のユーザーからの3つの回答。

バウンティ最終日のステータス:まだ新しい回答はありません。答えが表示されない場合、私はイスラム教徒のベン・ダウに賞金を授与する必要があります。

4

4 に答える 4

9

コンパイラはVB.NETで警告を表示してより良い処理を実行すると思いますが、それでも十分に進んでいるとは思いません。残念ながら、「正しいこと」には、おそらく有用な可能性のあるものを禁止する(2つの共変または反変のジェネリック型パラメーターを使用して同じインターフェースを実装する)か、言語に新しいものを導入する必要があります。

HungryWolf現状では、コンパイラが現在エラーを割り当てることができる場所は、クラス以外にはありません。これは、クラスが、あいまいになる可能性のあることを行う方法を知っていると主張しているポイントです。それは述べています

ICloneable私は、特定の方法で、またはそれを実装または継承するものを食べる方法を知っています。

そして、私はまたIConvertible、特定の方法で、またはそれを実装または継承するものを食べる方法を知っています。

ただし、プレート上でとの両方であるものを受け取った場合にすべきかについては決して述べていません。のインスタンスが与えられた場合、コンパイラは「ねえ、ここで何をすべきかわからない!」と確実に言うことができるので、これはコンパイラに何の不満も引き起こしません。。しかし、インスタンスが与えられると、コンパイラに悲しみを与えます。コンパイラーは、変数内のオブジェクトの実際のタイプが何であるかを認識していませんが、確実に実装しているだけですICloneableIConvertibleHungryWolfICanEat<string>ICanEat<string>

残念ながら、aHungryWolfがその変数に格納されると、まったく同じインターフェイスが2回あいまいに実装されます。したがって、確かに、呼び出しようとしてエラーをスローすることはできません。そのメソッドが存在し、変数ICanEat<string>.Eat(string)に配置できる他の多くのオブジェクトに対して完全に有効であるためです( batwadはすでに彼の回答の1つでこれについて言及しています)。ICanEat<string>

さらに、コンパイラーはHungryWolf、変数へのオブジェクトの割り当てICanEat<string>があいまいであると文句を言う可能性がありますが、2つのステップでそれが発生するのを防ぐことはできません。Aは変数HungryWolfに割り当てることがICanEat<IConvertible>でき、それを他のメソッドに渡して、最終的にICanEat<string>変数に割り当てることができます。これらは両方とも完全に合法的な割り当てであり、コンパイラがどちらかについて文句を言うことは不可能です。

したがって、オプション1HungryWolfは、クラスが両方ICanEat<IConvertible>を実装することを禁止することです。またICanEat<ICloneable>ICanEatジェネリック型パラメーターが反変である場合、これら2つのインターフェースは統合される可能性があるためです。ただし、これにより、代替の回避策なしで有用なものをコーディングする機能が削除されます。

オプション2は、残念ながら、ILとCLRの両方のコンパイラを変更する必要があります。これにより、クラスは両方のインターフェイスを実装できますが、ジェネリック型パラメーターが両方のインターフェイスを実装HungryWolfするインターフェイスインターフェイスの実装も必要になります。ICanEat<IConvertible & ICloneable>これはおそらく最良の構文ではありません(このEat(T)メソッドのシグネチャはどのように見えますEat(IConvertible & ICloneable food)か?)。おそらく、より良い解決策は、クラスの定義が次のようになるように、クラスを実装するときに自動生成されたジェネリック型にすることです。

class HungryWolf:
    ICanEat<ICloneable>, 
    ICanEat<IConvertible>, 
    ICanEat<TGenerated_ICloneable_IConvertible>
        where TGenerated_ICloneable_IConvertible: IConvertible, ICloneable {
    // implementation
}

次に、ILを変更して、callvirt命令の汎用クラスと同じようにインターフェイス実装タイプを構築できるようにする必要があります。

.class auto ansi nested private beforefieldinit HungryWolf 
    extends 
        [mscorlib]System.Object
    implements 
        class NamespaceOfApp.Program/ICanEat`1<class [mscorlib]System.ICloneable>,
        class NamespaceOfApp.Program/ICanEat`1<class [mscorlib]System.IConvertible>,
        class NamespaceOfApp.Program/ICanEat`1<class ([mscorlib]System.IConvertible, [mscorlib]System.ICloneable>)!TGenerated_ICloneable_IConvertible>

次に、CLRは、のジェネリック型パラメーターとしてwithcallvirtのインターフェイス実装を構築し、他のインターフェイス実装よりも一致するかどうかを確認することにより、命令を処理する必要があります。HungryWolfstringTGenerated_ICloneable_IConvertible

共分散の場合、実装する必要のある追加のインターフェイスは制約付きのジェネリック型パラメーターである必要はなく、コンパイル時に既知の2つの他の型の中で最も派生的な基本型である必要があるため、これらすべてが簡単になります。

同じインターフェースが2回以上実装されると、実装する必要のある追加のインターフェースの数は指数関数的に増加しますが、これは、単一のクラスに複数の反変(または共変)を実装する際の柔軟性と型安全性のコストになります。

これがフレームワークに組み込まれるとは思えませんが、特に新しい言語の複雑さは、現在危険なことを実行したいクラスに常に自己完結しているため、これが私の好ましい解決策になります。


編集:共通のインターフェースも考慮に入れる必要があるという事実のために、共分散は反変性よりも単純ではないことを私に思い出させてくれたJeppe
に 感謝します。との場合、最大の共通点のセットは{、、}になります(でカバーさます)。stringchar[]objectICloneableIEnumerable<char>IEnumerableIEnumerable<char>

ただし、これには、インターフェイスのジェネリック型パラメーター制約の新しい構文が必要になります。これは、ジェネリック型パラメーターが必要なのは

  • 指定されたクラスまたは指定されたインターフェイスの少なくとも1つを実装するクラスから継承する
  • 指定されたインターフェースの少なくとも1つを実装する

おそらく次のようなものです:

interface ICanReturn<out T> where T: class {
}

class ReturnStringsOrCharArrays: 
    ICanReturn<string>, 
    ICanReturn<char[]>, 
    ICanReturn<TGenerated_String_ArrayOfChar>
        where TGenerated_String_ArrayOfChar: object|ICloneable|IEnumerable<char> {
}

この場合のジェネリック型パラメーターTGenerated_String_ArrayOfChar(1つ以上のインターフェースが共通である場合)はobject、共通の基本クラスがすでにobject;から派生している場合でも、常にとして扱われる必要があります。共通型は、共通基本クラスから継承せずに共通インターフェースを実装する可能性があるためです。

于 2013-01-06T17:08:10.583 に答える
6

このような場合、コードが正しく、両方の内部型から同時に継承しないすべての型で正常に実行されるため、コンパイラエラーを生成できませんでした。クラスとインターフェイスから同時に継承する場合も、問題は同じです。(つまりobject、コードで基本クラスを使用します)。

何らかの理由で、VB.Netコンパイラは次のような場合に警告をスローします

インターフェイス「ICustom(OfFoo)」は、「インターフェイスICustom(Of In T)」の「In」および「Out」パラメータのために、別の実装されたインターフェイス「ICustom(OfBoo)」とあいまいです。

C#コンパイラも同様の警告をスローする必要があることに同意します。このStackOverflowの質問を確認してください。Lippert氏は、ランタイムが1つを選択し、この種のプログラミングは避けるべきであることを確認しました。

于 2012-11-27T15:33:47.740 に答える
4

これは、私が思いついたばかりの警告やエラーがないことを正当化するための1つの試みであり、私は飲んでいません!

コードをさらに抽象化します。意図は何ですか?

ICanEat<string> beast = SomeFactory.CreateRavenousBeast();
beast.Eat("sheep");

あなたは何かを食べています。獣が実際にそれを獣まで食べる方法。スープを与えられたら、スプーンを使うことにするかもしれません。羊を食べるのにナイフとフォークを使うかもしれません。それはそれをその歯で細かく裂くかもしれません。しかし、要点は、このクラスの呼び出し元として、私たちはそれがどのように食べるかを気にせず、ただそれを食べさせたいだけです。

最終的には、与えられたものをどのように食べるかを決めるのは獣次第です。獣がそれを食べることができるICloneableと決定した場合、IConvertible両方の場合をどのように処理するかを理解するのは獣次第です。なぜゲームキーパーは動物が食事をどのように食べるかを気にする必要がありますか?

コンパイル時エラーが発生した場合は、新しいインターフェイスの実装を重大な変更にします。たとえば、として出荷HungryWolfICanEat<ICloneable>、数か月後に追加ICanEat<IConvertible>すると、両方のインターフェイスを実装するタイプで使用されていたすべての場所が壊れます。

そしてそれは両方の道を切ります。一般的な議論が実装するだけなら、ICloneableそれはオオカミと非常にうまく機能するでしょう。テストに合格し、発送します。ウルフさん、どうもありがとうございました。来年IConvertibleはあなたのタイプとBANGにサポートを追加します!本当に、それほど役に立たないエラーです。

于 2012-11-27T16:21:05.530 に答える
2

別の説明:あいまいさはないため、コンパイラーの警告はありません。

我慢して。

ICanEat<string> wolf = new HungryWolf();
wolf.Eat("sheep");

これらのパラメーターでICanEat<T>呼び出されるメソッドは1つしかないため、エラーは発生しません。Eatしかし、代わりにこれを言うと、エラーが発生ます:

HungryWolf wolf = new HungryWolf();
wolf.Eat("sheep");

HungryWolfあいまいですが、ではありませんICanEat<T>。コンパイラエラーを予期する場合、呼び出された時点で変数の値を調べ、Eatそれがあいまいであるかどうかを判断するようにコンパイラに要求します。これは必ずしも推測できるものではありません。UnambiguousCowを実装するだけのクラスを考えてみましょうICanEat<string>

ICanEat<string> beast;

if (somethingUnpredictable)
    beast = new HungryWolf();
else
    beast = new UnambiguousCow();

beast.Eat("dinner");

どこでどのようにコンパイラエラーが発生すると予想しますか?

于 2012-11-28T11:29:05.830 に答える