11

苦情は次のとおりです。

interface IInvariant<TInv> {}
interface ICovariant<out TCov> {
    IInvariant<TCov> M(); // The covariant type parameter `TCov'
                          // must be invariantly valid on
                          // `ICovariant<TCov>.M()'
}
interface IContravariant<in TCon> {
    void M(IInvariant<TCon> v); // The contravariant type parameter
                                // `TCon' must be invariantly valid
                                // on `IContravariant<TCon>.M()'
}

しかし、これがタイプセーフではない場所を想像することはできません。(中略*) これが許可されていない理由ですか、それとも私が認識していない型の安全性に違反する他のケースがあるのでしょうか?


* 私の最初の考えは確かに複雑でしたが、それにもかかわらず、回答は非常に徹底しており、@Theodoros Chatzigiannakisは私の最初の仮定を驚くほど正確に分析しました。

振り返ってみると、 の型シグネチャが に代入されたときにの型シグネチャが のICovariant::Mままであると誤って想定していたことに気付きました。次に、それを に割り当てると、から来ると問題ないように見えますが、もちろん違法です。この最後の、明らかに違法なキャストを禁止しないのはなぜですか? (ので、私は考えました)Func<IInvariant<Derived>>ICovariant<Derived>ICovariant<Base>MFunc<IInvariant<Base>>ICovariant<Base>

エリック・リッパートも指摘しているように、この誤った接線の推測は質問から逸れていると思いますが、歴史的な目的のために、切り取られた部分は次のとおりです。

私にとって最も直感的な説明はICovariant、例として、共変は、メソッドがどこかにキャストされる可能性があるTCovことを意味し、inの不変性に違反するということです。ただし、この含意は必要ではないようです。onの不変性は、 のキャストを許可しないことで簡単に強制できます。IInvariant<TCov> M()IInvariant<TSuper> M()TSuper super TCovTInvIInvariantIInvariantTInvM

4

3 に答える 3

5

これまでの回答のいずれかで、あなたの質問に対する回答が実際に得られたかどうかはわかりません。

クラス型パラメータの分散が、そのメソッドの戻り値/引数型パラメータの分散と一致しなければならないのはなぜですか?

そうではないので、質問は誤った前提に基づいています。実際のルールは次のとおりです。

https://blogs.msdn.microsoft.com/ericlippert/2009/12/03/exact-rules-for-variance-validity/

今考えてみましょう:

interface IInvariant<TInv> {}
interface ICovariant<out TCov> {
   IInvariant<TCov> M(); // Error
}

これが許可されていない理由ですか、それとも私が認識していない型の安全性に違反する他のケースがありますか?

私はあなたの説明に従っていないので、あなたの説明を参照せずに、なぜこれが許可されていないのかを述べましょう. ここで、これらの型を同等の型に置き換えてみましょう。 IInvariant<TInv>T で不変な任意の型にすることができます。たとえば、次のようにしますICage<TCage>

interface ICage<TAnimal> {
  TAnimal Remove();
  void Insert(TAnimal contents);
}

Cage<TAnimal>を実装する型があるかもしれませんICage<TAnimal>

ICovariant<T>そして、置き換えましょう

interface ICageFactory<out T> {
   ICage<T> MakeCage();
}

インターフェースを実装しましょう:

class TigerCageFactory : ICageFactory<Tiger> 
{ 
  public ICage<Tiger> MakeCage() { return new Cage<Tiger>(); }
}

すべてが順調に進んでいます。ICageFactoryは共変であるため、これは合法です。

ICageFactory<Animal> animalCageFactory = new TigerCageFactory();
ICage<Animal> animalCage = animalCageFactory.MakeCage();
animalCage.Insert(new Fish());

そして、魚をトラの檻に入れただけです。そこのすべてのステップは完全に合法であり、型システム違反に終わりました。私たちが到達した結論はICageFactory、そもそも共変にすることは合法ではなかったに違いないということです。

反変の例を見てみましょう。それは基本的に同じです:

interface ICageFiller<in T> {
   void Fill(ICage<T> cage);
}

class AnimalCageFiller : ICageFiller<Animal> {
  public void Fill(ICage<Animal> cage)
  {
    cage.Insert(new Fish());
  }
}

そして今、インターフェイスは反変なので、これは合法です:

ICageFiller<Tiger> tigerCageFiller = new AnimalCageFiller();
tigerCageFiller.Fill(new Cage<Tiger>());

もう一度魚をトラの檻に入れました。繰り返しますが、そもそも型を反変にすることは違法だったに違いないと結論付けています。

それでは、これらが違法であることをどのように知るかという問題を考えてみましょう。最初のケースでは、

interface ICageFactory<out T> {
   ICage<T> MakeCage();
}

関連するルールは次のとおりです。

すべての非 void インターフェイス メソッドの戻り値の型は、共変的に有効でなければなりません。

ICage<T>「共変的に有効」ですか?

次の場合、型は共変的に有効です: 1) ポインター型、または非ジェネリック クラス... NOPE 2) 配列型... NOPE 3) ジェネリック型パラメーター型 ... NOPE 4) 構築されたクラス、構造体、列挙型、インターフェイス、またはデリゲート型X<T1, … Tk>はい! ... i 番目の型パラメーターが不変として宣言されている場合、Ti は不変に有効でなければなりません。

TAnimalは in で不変だったICage<TAnimal>ので、 in は不変に有効でなければなりませんTICage<T>それは...ですか?いいえ。不変に有効であるためには、共変と反変の両方で有効でなければなりませんが、共変でのみ有効です。

したがって、これはエラーです。

反変の場合の分析は演習として残します。

于 2016-05-17T20:07:15.573 に答える
1

クラス型パラメータの分散が、そのメソッドの戻り値/引数型パラメータの分散と一致しなければならないのはなぜですか?

そうではありません!

戻り値の型と引数の型は、外側の型の分散と一致する必要はありません。あなたの例では、それらは両方の囲み型に対して共変である必要があります。直感に反するように聞こえますが、その理由は以下の説明で明らかになります。


提案されたソリューションが有効でない理由

共変は、メソッドがinの不変性に違反するwhereにキャストできることTCovを意味します。ただし、この含意は必要ではないようです。onの不変性は、 のキャストを許可しないことで簡単に強制できます。IInvariant<TCov> M()IInvariant<TSuper> M()TSuper super TCovTInvIInvariantIInvariantTInvM

  • あなたが言っているのは、バリアント型パラメーターを持つジェネリック型は、同じジェネリック型定義の別の型と異なる型パラメーターに割り当てることができるということです。その部分は正しいです。
  • しかし、潜在的なサブタイプ違反の問題を回避するために、メソッドの見かけのシグネチャはプロセスで変更されるべきではないと言っています。それは正しくありません。

たとえばICovariant<string>、メソッドがありIInvariant<string> M()ます。「」のキャストを許可しないということは、が に割り当てられた場合でも、署名付きのメソッドを保持することMを意味します。それが許可されている場合、この完全に有効な方法には問題があります。ICovariant<string>ICovariant<object>IInvariant<string> M()

void Test(ICovariant<object> arg)
{
    var obj = arg.M();
}

obj変数の型について、コンパイラはどの型を推測する必要がありますか? それはすべきIInvariant<string>ですか?IInvariant<Window>またはIInvariant<UTF8Encoding>またはしないのはなぜIInvariant<TcpClient>ですか?それらはすべて有効です。自分の目で確かめてください。

Test(new CovariantImpl<string>());
Test(new CovariantImpl<Window>());
Test(new CovariantImpl<UTF8Encoding>());
Test(new CovariantImpl<TcpClient>());

明らかに、メソッド ( ) の静的に既知の戻り値の型は、オブジェクトのランタイム型によって実装されM()たインターフェイス ( ) に依存することはできません!ICovariant<>

したがって、ジェネリック型が、より一般的な型引数を持つ別のジェネリック型に割り当てられる場合、対応する型パラメーターを使用するメンバー シグネチャも、より一般的なものに変更する必要があります。型の安全性を維持したい場合、これを回避する方法はありません。それでは、それぞれのケースで「より一般的な」とはどういう意味かを見てみましょう。


共変であるICovariant<TCov>必要がある理由IInvariant<TInv>

の型引数のstring場合、コンパイラは次の具象型を「認識」します。

interface ICovariant<string>
{
    IInvariant<string> M();
}

そして (上で見たように) の型引数のobject場合、コンパイラは代わりにこの具象型を「認識」します。

interface ICovariant<object>
{
    IInvariant<object> M();
}

前のインターフェースを実装する型を想定します。

class MyType : ICovariant<string>
{
    public IInvariant<string> M() 
    { /* ... */ }
}

この型での の実際の実装はM()、 を返すことのみに関係し、IInvariant<string>分散は気にしないことに注意してください。これを覚えておいてください!

共変の型パラメーターを作成することにより、次のように割り当て可能である必要がICovariant<TCov>あると主張しています。ICovariant<string>ICovariant<object>

ICovariant<string> original = new MyType();
ICovariant<object> covariant = original;

...そして、あなたは今これを行うことができる主張しています:

IInvariant<string> r1 = original.M();
IInvariant<object> r2 = covariant.M();

original.M()covariant.M()は同じメソッドの呼び出しであることを思い出してください。そして、実際のメソッドの実装は、 を返す必要があることしか認識していませんInvariant<string>

したがって、後者の呼び出しの実行中のある時点で、IInvariant<string>(実際のメソッドによって返される) を暗黙的に (IInvariant<object>共変署名が約束する) に変換します。これを行うには、IInvariant<string>を に割り当て可能にする必要がありますIInvariant<object>

一般化するには、同じ関係が everyIInvariant<S>IInvariant<T>where に適用される必要がありますS : T。そして、それはまさに共変型パラメーターの説明です。


共変であるIContravariant<TCon> 必要がある理由IInvariant<TInv>

の型引数のobject場合、コンパイラは次の具象型を「認識」します。

interface IContravariant<object>
{
    void M(IInvariant<object> v); 
}

の型引数のstring場合、コンパイラはこの具象型を「認識」します。

interface IContravariant<string>
{
    void M(IInvariant<string> v); 
}

前のインターフェースを実装する型を想定します。

class MyType : IContravariant<object>
{
    public void M(IInvariant<object> v)
    { /* ... */ }
}

繰り返しますが、 の実際の実装では、ユーザーからM()が取得されることを前提としておりIInvariant<object>、差異は考慮されていないことに注意してください。

の型パラメーターを作成することで、 がこのように割り当て可能であるIContravariant<TCon>と主張しています...IContravariant<object>IContravariant<string>

IContravariant<object> original = new MyType();
IContravariant<string> contravariant = original;

...そして、あなたは今これを行うことができる主張しています:

IInvariant<object> arg = Something();
original.M(arg);
IInvariant<string> arg2 = SomethingElse();
contravariant.M(arg2);

繰り返しますが、original.M(arg)contravariant.M(arg2)は同じメソッドの呼び出しです。そのメソッドの実際の実装では、IInvariant<object>.

したがって、後者の呼び出しの実行中のある時点で、IInvariant<string>(反変の署名が期待するもの) をIInvariant<object>(実際のメソッドが期待するもの) に暗黙的に変換します。これを行うには、IInvariant<string>を に割り当て可能にする必要がありますIInvariant<object>

一般化すると、 everyはwhereIInvariant<S>に割り当て可能である必要があります。そのため、共変型パラメーターをもう一度見ていきます。IInvariant<T>S : T


ここで、なぜ不一致があるのか​​疑問に思うかもしれません。共分散と反分散の二重性はどこに行ったのですか? それはまだそこにありますが、あまり明白ではない形式です:

  • 出力の側にいる場合、参照される型の分散は、それを囲む型の分散と同じ方向になります。この場合、囲んでいる型は共変または不変になる可能性があるため、参照される型もそれぞれ共変または不変でなければなりません。
  • 入力の側にいる場合、参照される型の分散は、囲んでいる型の分散の方向と逆になります。この場合、囲んでいる型は反変または不変になる可能性があるため、参照される型はそれぞれ共変または不変でなければなりません。
于 2016-05-17T16:33:58.007 に答える