[英]Why does the variance of a class type parameter have to match the variance of its methods' return/argument type parameters?
以下提出了投诉:
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<Derived>
被分配给ICovariant<Base>
时,我错误地认为ICovariant::M
的类型签名仍然是Func<IInvariant<Derived>>
。 然后,将M
分配给Func<IInvariant<Base>>
看起来很好,来自ICovariant<Base>
,但当然是非法的。 为什么不禁止最后这个显然是非法的演员? (所以我认为)
正如埃里克·利珀特 ( Eric Lippert)所指出的那样,我觉得这种错误和切向猜测会减少这个问题,但出于历史目的,这个被剪切的部分:
对我来说最直观的解释是,以
ICovariant
为例,协变性TCov
意味着方法IInvariant<TCov> M()
可以被转换为某些IInvariant<TSuper> M()
,其中TSuper super TCov
,这违反了不变性IInvariant
的TInv
。 然而,这种暗示似乎并不是必要的:通过禁止M
的演员,可以很容易地强制执行IInvariant
对TInv
的不变性。
让我们看一个更具体的例子。 我们将对这些接口进行一些实现:
class InvariantImpl<T> : IInvariant<T>
{
}
class CovariantImpl<T> : ICovariant<T>
{
public IInvariant<T> M()
{
return new InvariantImpl<T>();
}
}
现在,让我们假设编译器没有抱怨这个并尝试以一种简单的方式使用它:
static IInvariant<object> Foo( ICovariant<object> o )
{
return o.M();
}
到现在为止还挺好。 o
是ICovariant<object>
,该接口保证我们有一个可以返回IInvariant<object>
。 我们不必在这里进行任何演员表或转换,一切都很好。 现在让我们调用方法:
var x = Foo( new CovariantImpl<string>() );
因为ICovariant
是协变的,所以这是一个有效的方法调用,我们可以用任何想要ICovariant<object>
的ICovariant<string>
替换因为该协方差。
但是我们遇到了问题。 在Foo
,我们调用ICovariant<object>.M()
并期望它返回一个IInvariant<object>
因为这就是ICovariant
接口所说的。 但它不能这样做,因为我们传递的实际实现实际上实现了ICovariant<string>
并且它的M
方法返回IInvariant<string>
,由于该接口的不变性,它与IInvariant<object>
无关 。 它们是完全不同的类型。
到目前为止,我不确定你的答案是否真的得到了答案。
为什么类类型参数的方差必须与其方法的返回/参数类型参数的方差相匹配?
它没有,所以问题是基于错误的前提。 实际规则如下:
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接口方法的返回类型必须是covariantly有效。
ICage<T>
“有效ICage<T>
”吗?
如果类型是:1)指针类型或非泛型类,则该类型是有效的... NOPE 2)数组类型... NOPE 3)泛型类型参数类型... NOPE 4)构造类,struct,enum,interface或delegate type
X<T1, … Tk>
YES! ...如果第i个类型参数被声明为不变量,那么Ti必须是不变的有效。
TAnimal
是不变ICage<TAnimal>
所以T
在ICage<T>
必须是有效的不变地。 是吗? 否。要有效目不暇接,它必须是有效的两个协变和contravariantly,但它是有效的只有协变。
因此这是一个错误。
对逆变情况进行分析是一项练习。
为什么类类型参数的方差必须与其方法的返回/参数类型参数的方差相匹配?
它没有!
返回类型和参数类型不需要与封闭类型的方差匹配。 在您的示例中,它们需要对两个封闭类型都是协变的。 这听起来违反直觉,但原因将在下面的解释中变得明显。
协变性
TCov
意味着方法IInvariant<TCov> M()
可以转换为某些IInvariant<TSuper> M()
,其中TSuper super TCov
,它违反了IInvariant
中TInv
的不变性。 然而,这种暗示似乎并不是必要的:通过禁止M
的演员,可以很容易地强制执行IInvariant
对TInv
的不变性。
例如, ICovariant<string>
有一个方法IInvariant<string> M()
。 “禁止M
ICovariant<string>
”意味着当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>
。
概括地说,相同的关系必须适用于每个IInvariant<S>
和IInvariant<T>
,其中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>
。
概括地说,每个IInvariant<S>
应该可以赋予IInvariant<T>
,其中S : T
。 所以我们再次查看协变类型参数。
现在你可能想知道为什么会出现不匹配。 协方差和逆变的二元性在哪里? 它仍然存在,但形式不太明显:
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.