[英]Why does the variance of a class type parameter have to match the variance of its methods' return/argument type parameters?
The following raises complaints: 以下提出了投诉:
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()'
}
but I can't imagine where this wouldn't be type-safe. 但我无法想象这不会是类型安全的。 (snip*) Is this the reason why this is disallowed, or is there some other case which violates type safety which I'm not aware of?
(剪断*)这是不允许这样做的原因,还是有其他违反类型安全的情况我不知道?
* My initial thoughts were admittedly convoluted, but despite this, the responses are very thorough, and @Theodoros Chatzigiannakis even dissected my initial assumptions with impressive accuracy. *我最初的想法确实令人费解,但尽管如此,答案非常彻底, @ Theodoros Chatzigiannakis甚至以令人印象深刻的准确性解剖了我的初步假设。
Alongside a good slap from retrospect, I realize that I had falsely assumed that the type signature of ICovariant::M
remains a Func<IInvariant<Derived>>
when its ICovariant<Derived>
is assigned to a ICovariant<Base>
. 除了回顾过去的好评之外,我意识到当我的
ICovariant<Derived>
被分配给ICovariant<Base>
时,我错误地认为ICovariant::M
的类型签名仍然是Func<IInvariant<Derived>>
。 Then, assigning that M
to Func<IInvariant<Base>>
would look fine coming from an ICovariant<Base>
, but would of course be illegal. 然后,将
M
分配给Func<IInvariant<Base>>
看起来很好,来自ICovariant<Base>
,但当然是非法的。 Why not just ban this last, obviously-illegal cast? 为什么不禁止最后这个显然是非法的演员? (so I thought)
(所以我认为)
I feel this false and tangential guess detracts from the question, as Eric Lippert also points out, but for historical purposes, the snipped part: 正如埃里克·利珀特 ( Eric Lippert)所指出的那样,我觉得这种错误和切向猜测会减少这个问题,但出于历史目的,这个被剪切的部分:
The most intuitive explanation to me is that, taking
ICovariant
as an example, the covariantTCov
implies that the methodIInvariant<TCov> M()
could be cast to someIInvariant<TSuper> M()
whereTSuper super TCov
, which violates the invariance ofTInv
inIInvariant
.对我来说最直观的解释是,以
ICovariant
为例,协变性TCov
意味着方法IInvariant<TCov> M()
可以被转换为某些IInvariant<TSuper> M()
,其中TSuper super TCov
,这违反了不变性IInvariant
的TInv
。 However, this implication doesn't seem necessary: the invariance ofIInvariant
onTInv
could easily be enforced by disallowing the cast ofM
.然而,这种暗示似乎并不是必要的:通过禁止
M
的演员,可以很容易地强制执行IInvariant
对TInv
的不变性。
Let's look at a more concrete example. 让我们看一个更具体的例子。 We'll make a couple implementations of these interfaces:
我们将对这些接口进行一些实现:
class InvariantImpl<T> : IInvariant<T>
{
}
class CovariantImpl<T> : ICovariant<T>
{
public IInvariant<T> M()
{
return new InvariantImpl<T>();
}
}
Now, let's assume that the compiler didn't complain about this and try to use it in a simple way: 现在,让我们假设编译器没有抱怨这个并尝试以一种简单的方式使用它:
static IInvariant<object> Foo( ICovariant<object> o )
{
return o.M();
}
So far so good. 到现在为止还挺好。
o
is ICovariant<object>
and that interface guarantees that we have a method that can return an IInvariant<object>
. o
是ICovariant<object>
,该接口保证我们有一个可以返回IInvariant<object>
。 We don't have to perform any casts or conversions here, everything is fine. 我们不必在这里进行任何演员表或转换,一切都很好。 Now let's call the method:
现在让我们调用方法:
var x = Foo( new CovariantImpl<string>() );
Because ICovariant
is covariant, this is a valid method call, we can substitute an ICovariant<string>
wherever something wants an ICovariant<object>
because of that covariance. 因为
ICovariant
是协变的,所以这是一个有效的方法调用,我们可以用任何想要ICovariant<object>
的ICovariant<string>
替换因为该协方差。
But we have a problem. 但是我们遇到了问题。 Inside
Foo
, we call ICovariant<object>.M()
and expect it to return an IInvariant<object>
because that's what the ICovariant
interface says it will do. 在
Foo
,我们调用ICovariant<object>.M()
并期望它返回一个IInvariant<object>
因为这就是ICovariant
接口所说的。 But it can't do that, because the actual implementation we've passed actually implements ICovariant<string>
and its M
method returns IInvariant<string>
, which has nothing to do with IInvariant<object>
due to the invariance of that interface. 但它不能这样做,因为我们传递的实际实现实际上实现了
ICovariant<string>
并且它的M
方法返回IInvariant<string>
,由于该接口的不变性,它与IInvariant<object>
无关 。 They are completely different types. 它们是完全不同的类型。
I'm not sure that you actually got your question answered in either of the answers so far. 到目前为止,我不确定你的答案是否真的得到了答案。
Why does the variance of a class type parameter have to match the variance of its methods' return/argument type parameters?
为什么类类型参数的方差必须与其方法的返回/参数类型参数的方差相匹配?
It doesn't, so the question is based on a false premise. 它没有,所以问题是基于错误的前提。 The actual rules are here:
实际规则如下:
https://blogs.msdn.microsoft.com/ericlippert/2009/12/03/exact-rules-for-variance-validity/ https://blogs.msdn.microsoft.com/ericlippert/2009/12/03/exact-rules-for-variance-validity/
Consider now: 现在考虑:
interface IInvariant<TInv> {}
interface ICovariant<out TCov> {
IInvariant<TCov> M(); // Error
}
Is this the reason why this is disallowed, or is there some other case which violates type safety which I'm not aware of?
这是不允许这样做的原因,还是有其他违反类型安全的情况我不知道?
I'm not following your explanation, so let's just say why this is disallowed without reference to your explanation. 我没有按照你的解释,所以我们只是说明为什么在不参考你的解释的情况下不允许这样做。 Here, let me replace these types with some equivalent types.
在这里,让我用一些等价的类型替换这些类型。
IInvariant<TInv>
can be any type that is invariant in T, let's say ICage<TCage>
: IInvariant<TInv>
可以是T中不变的任何类型,假设ICage<TCage>
:
interface ICage<TAnimal> {
TAnimal Remove();
void Insert(TAnimal contents);
}
And maybe we have a type Cage<TAnimal>
that implements ICage<TAnimal>
. 也许我们有一个类型
Cage<TAnimal>
来实现ICage<TAnimal>
。
And let's replace ICovariant<T>
with 让我们替换
ICovariant<T>
interface ICageFactory<out T> {
ICage<T> MakeCage();
}
Let's implement the interface: 让我们实现界面:
class TigerCageFactory : ICageFactory<Tiger>
{
public ICage<Tiger> MakeCage() { return new Cage<Tiger>(); }
}
Everything is going so well. 一切都进展顺利。
ICageFactory
is covariant, so this is legal: ICageFactory
是协变的,所以这是合法的:
ICageFactory<Animal> animalCageFactory = new TigerCageFactory();
ICage<Animal> animalCage = animalCageFactory.MakeCage();
animalCage.Insert(new Fish());
And we just put a fish into a tiger cage. 我们只是把鱼放进虎笼里。 Every step there was perfectly legal and we ended up with a type system violation.
那里的每一步都完全合法,我们最终违反了类型系统。 The conclusion we reach is that it must not have been legal to make
ICageFactory
covariant in the first place. 我们得出的结论是,首先使
ICageFactory
协变一定不合法。
Let's look at your contravariant example; 让我们来看看你的逆变例子; it's basically the same:
它基本相同:
interface ICageFiller<in T> {
void Fill(ICage<T> cage);
}
class AnimalCageFiller : ICageFiller<Animal> {
public void Fill(ICage<Animal> cage)
{
cage.Insert(new Fish());
}
}
And now, the interface is contravariant so this is legal: 现在,界面是逆变的,所以这是合法的:
ICageFiller<Tiger> tigerCageFiller = new AnimalCageFiller();
tigerCageFiller.Fill(new Cage<Tiger>());
Once again we have put a fish into a tiger cage. 我们又一次将鱼放入虎笼中。 Once again we conclude that it must have been illegal to make the type contravariant in the first place.
我们再次得出结论,首先制造类型逆变一定是非法的。
So now let's consider the question of how we know that these are illegal. 现在让我们考虑一下我们如何知道这些都是非法的问题。 In the first case we have
在第一种情况下我们有
interface ICageFactory<out T> {
ICage<T> MakeCage();
}
And the relevant rule is: 相关规则是:
The return types of all non-void interface methods must be valid covariantly.
所有非void接口方法的返回类型必须是covariantly有效。
Is ICage<T>
"valid covariantly"? ICage<T>
“有效ICage<T>
”吗?
A type is valid covariantly if it is: 1) a pointer type, or a non-generic class... NOPE 2) An array type... NOPE 3) A generic type parameter type ... NOPE 4) A constructed class, struct, enum, interface or delegate type
X<T1, … Tk>
YES!如果类型是:1)指针类型或非泛型类,则该类型是有效的... NOPE 2)数组类型... NOPE 3)泛型类型参数类型... NOPE 4)构造类,struct,enum,interface或delegate type
X<T1, … Tk>
YES! ... If the ith type parameter was declared as invariant, then Ti must be valid invariantly....如果第i个类型参数被声明为不变量,那么Ti必须是不变的有效。
TAnimal
was invariant in ICage<TAnimal>
, So T
in ICage<T>
must be valid invariantly. TAnimal
是不变ICage<TAnimal>
所以T
在ICage<T>
必须是有效的不变地。 Is it? 是吗? No. To be valid invariantly it must be valid both covariantly and contravariantly, but it is valid only covariantly.
否。要有效目不暇接,它必须是有效的两个协变和contravariantly,但它是有效的只有协变。
Therefore this is an error. 因此这是一个错误。
Doing the analysis for the contravariant case is left as an exercise. 对逆变情况进行分析是一项练习。
Why does the variance of a class type parameter have to match the variance of its methods' return/argument type parameters?
为什么类类型参数的方差必须与其方法的返回/参数类型参数的方差相匹配?
It doesn't! 它没有!
The return types and the argument types don't need to match the variance of the enclosing type. 返回类型和参数类型不需要与封闭类型的方差匹配。 In your example, they need to be covariant for both enclosing types.
在您的示例中,它们需要对两个封闭类型都是协变的。 It sounds counter-intuitive, but the reasons will become apparent in the explanation below.
这听起来违反直觉,但原因将在下面的解释中变得明显。
the covariant
TCov
implies that the methodIInvariant<TCov> M()
could be cast to someIInvariant<TSuper> M()
whereTSuper super TCov
, which violates the invariance ofTInv
inIInvariant
.协变性
TCov
意味着方法IInvariant<TCov> M()
可以转换为某些IInvariant<TSuper> M()
,其中TSuper super TCov
,它违反了IInvariant
中TInv
的不变性。 However, this implication doesn't seem necessary: the invariance ofIInvariant
onTInv
could easily be enforced by disallowing the cast ofM
.然而,这种暗示似乎并不是必要的:通过禁止
M
的演员,可以很容易地强制执行IInvariant
对TInv
的不变性。
For example, ICovariant<string>
has a method IInvariant<string> M()
. 例如,
ICovariant<string>
有一个方法IInvariant<string> M()
。 "Disallowing the cast of M
" would mean that when ICovariant<string>
is assigned to ICovariant<object>
, it still retains the method with the signature IInvariant<string> M()
. “禁止
M
ICovariant<string>
”意味着当ICovariant<string>
被分配给ICovariant<object>
,它仍然保留带有签名IInvariant<string> M()
。 If that were allowed, then this perfectly valid method would have a problem: 如果允许,那么这个完全有效的方法会有问题:
void Test(ICovariant<object> arg)
{
var obj = arg.M();
}
What type should the compiler infer for the type of the obj
variable? 编译器应该为
obj
变量的类型推断出什么类型? Should it be IInvariant<string>
? 应该是
IInvariant<string>
吗? Why not IInvariant<Window>
or IInvariant<UTF8Encoding>
or IInvariant<TcpClient>
? 为什么不使用
IInvariant<Window>
或IInvariant<UTF8Encoding>
或IInvariant<TcpClient>
? All of them can be valid, see for yourself: 所有这些都是有效的,请亲自看看:
Test(new CovariantImpl<string>());
Test(new CovariantImpl<Window>());
Test(new CovariantImpl<UTF8Encoding>());
Test(new CovariantImpl<TcpClient>());
Clearly, the statically known return type of a method ( M()
) can't possibly depend on an interface ( ICovariant<>
) implemented by the runtime type of the object! 显然, 静态已知的方法返回类型 (
M()
)不可能依赖于由对象的运行时类型实现的接口( ICovariant<>
) !
So when the generic type is assigned to another generic type with more general type arguments, the member signatures that use the corresponding type parameters must necessarily change to something more general as well. 因此,当泛型类型被分配给具有更多通用类型参数的另一个泛型类型时,使用相应类型参数的成员签名也必须更改为更通用的类型。 There's no way around it if we want to maintain type safety.
如果我们想要保持类型安全,就无法绕过它。 Now let's see what "more general" means in each case.
现在让我们看看在每种情况下“更一般”意味着什么。
ICovariant<TCov>
requires IInvariant<TInv>
to be covariant ICovariant<TCov>
要求IInvariant<TInv>
是协变的 For a type argument of string
, the compiler "sees" this concrete type: 对于
string
的类型参数,编译器“看到”这个具体类型:
interface ICovariant<string>
{
IInvariant<string> M();
}
And (as we saw above) for a type argument of object
, the compiler "sees" this concrete type instead: 并且(如上所述)
object
的类型参数,编译器“看到”这个具体类型:
interface ICovariant<object>
{
IInvariant<object> M();
}
Assume a type that implements the former interface: 假设实现前一个接口的类型:
class MyType : ICovariant<string>
{
public IInvariant<string> M()
{ /* ... */ }
}
Notice that the actual implementation of M()
in this type is only concerned with returning an IInvariant<string>
and it doesn't care about variance. 请注意, 此类型中
M()
的实际实现仅涉及返回IInvariant<string>
,而不关心方差。 Keep this in mind! 牢记这一点!
Now by making the type parameter of ICovariant<TCov>
covariant, you are asserting that ICovariant<string>
should be assignable to ICovariant<object>
like this: 现在通过创建
ICovariant<TCov>
协变的类型参数,您断言ICovariant<string>
应该可以像这样分配给ICovariant<object>
:
ICovariant<string> original = new MyType();
ICovariant<object> covariant = original;
...and you are also asserting that you can now do this: ...而且你也断言你现在可以这样做:
IInvariant<string> r1 = original.M();
IInvariant<object> r2 = covariant.M();
Remember, original.M()
and covariant.M()
are calls to the same method. 请记住,
original.M()
和covariant.M()
是对同一方法的调用。 And the actual method implementation only knows that it should return an Invariant<string>
. 实际的方法实现只知道它应该返回一个
Invariant<string>
。
So, at some point during the execution of the latter call, we are implicitly converting an IInvariant<string>
(returned by the actual method) to an IInvariant<object>
(which is what the covariant signature promises). 因此,在执行后一个调用的某个时刻,我们隐式地将
IInvariant<string>
(由实际方法返回)转换为IInvariant<object>
(这是协变签名所承诺的)。 For this to happen, IInvariant<string>
must be assignable to IInvariant<object>
. 为此,
IInvariant<string>
必须可分配给IInvariant<object>
。
To generalize, the same relationship must apply for every IInvariant<S>
and IInvariant<T>
where S : T
. 概括地说,相同的关系必须适用于每个
IInvariant<S>
和IInvariant<T>
,其中S : T
。 And that's exactly the description of a covariant type parameter. 这正是协变类型参数的描述。
IContravariant<TCon>
also requires IInvariant<TInv>
to be covariant IContravariant<TCon>
还要求IInvariant<TInv>
是协变的 For a type argument of object
, the compiler "sees" this concrete type: 对于
object
的类型参数,编译器“看到”这个具体类型:
interface IContravariant<object>
{
void M(IInvariant<object> v);
}
And for a type argument of string
, the compiler "sees" this concrete type: 对于
string
的类型参数,编译器“看到”这个具体类型:
interface IContravariant<string>
{
void M(IInvariant<string> v);
}
Assume a type that implements the former interface: 假设实现前一个接口的类型:
class MyType : IContravariant<object>
{
public void M(IInvariant<object> v)
{ /* ... */ }
}
Again, notice that the actual implementation of M()
assumes that it will get an IInvariant<object>
from you and it doesn't care about variance. 再次注意,
M()
的实际实现假设它将从您那里获得一个IInvariant<object>
并且它不关心方差。
Now by making the type parameter of IContravariant<TCon>
, you are asserting that IContravariant<object>
should be assignable to IContravariant<string>
like this... 现在通过创建
IContravariant<TCon>
的类型参数,您断言IContravariant<object>
应该像这样分配给IContravariant<string>
...
IContravariant<object> original = new MyType();
IContravariant<string> contravariant = original;
...and you are also asserting that you can now do this: ...而且你也断言你现在可以这样做:
IInvariant<object> arg = Something();
original.M(arg);
IInvariant<string> arg2 = SomethingElse();
contravariant.M(arg2);
Again, original.M(arg)
and contravariant.M(arg2)
are calls to the same method. 同样,
original.M(arg)
和contravariant.M(arg2)
是对同一方法的调用。 The actual implementation of that method expects us to pass anything that's an IInvariant<object>
. 该方法的实际实现要求我们传递任何
IInvariant<object>
。
So, at some point during the execution of the latter call, we are implicitly converting an IInvariant<string>
(which is what the contravariant signature expects from us) to an IInvariant<object>
(which is what the actual method expects). 因此,在执行后一个调用期间的某个时刻,我们隐式地将
IInvariant<string>
(这是逆变量签名对我们的期望)转换为IInvariant<object>
(这是实际方法所期望的)。 For this to happen, IInvariant<string>
must be assignable to IInvariant<object>
. 为此,
IInvariant<string>
必须可分配给IInvariant<object>
。
To generalize, every IInvariant<S>
should be assignable to IInvariant<T>
where S : T
. 概括地说,每个
IInvariant<S>
应该可以赋予IInvariant<T>
,其中S : T
。 So we're looking at a covariant type parameter again. 所以我们再次查看协变类型参数。
Now you may be wondering why there is a mismatch. 现在你可能想知道为什么会出现不匹配。 Where did the duality of covariance and contravariance go?
协方差和逆变的二元性在哪里? It's still there, but in a less obvious form:
它仍然存在,但形式不太明显:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.