クラス型パラメータの分散が、そのメソッドの戻り値/引数型パラメータの分散と一致しなければならないのはなぜですか?
そうではありません!
戻り値の型と引数の型は、外側の型の分散と一致する必要はありません。あなたの例では、それらは両方の囲み型に対して共変である必要があります。直感に反するように聞こえますが、その理由は以下の説明で明らかになります。
提案されたソリューションが有効でない理由
共変は、メソッドがinの不変性に違反するwhereにキャストできることTCov
を意味します。ただし、この含意は必要ではないようです。onの不変性は、 のキャストを許可しないことで簡単に強制できます。IInvariant<TCov> M()
IInvariant<TSuper> M()
TSuper super TCov
TInv
IInvariant
IInvariant
TInv
M
- あなたが言っているのは、バリアント型パラメーターを持つジェネリック型は、同じジェネリック型定義の別の型と異なる型パラメーターに割り当てることができるということです。その部分は正しいです。
- しかし、潜在的なサブタイプ違反の問題を回避するために、メソッドの見かけのシグネチャはプロセスで変更されるべきではないと言っています。それは正しくありません。
たとえば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
ここで、なぜ不一致があるのか疑問に思うかもしれません。共分散と反分散の二重性はどこに行ったのですか? それはまだそこにありますが、あまり明白ではない形式です:
- 出力の側にいる場合、参照される型の分散は、それを囲む型の分散と同じ方向になります。この場合、囲んでいる型は共変または不変になる可能性があるため、参照される型もそれぞれ共変または不変でなければなりません。
- 入力の側にいる場合、参照される型の分散は、囲んでいる型の分散の方向と逆になります。この場合、囲んでいる型は反変または不変になる可能性があるため、参照される型はそれぞれ共変または不変でなければなりません。