为什么类类型参数的方差必须与其方法的返回/参数类型参数的方差相匹配?
它没有!
返回类型和参数类型不需要匹配封闭类型的方差。在您的示例中,它们需要对两种封闭类型都是协变的。这听起来违反直觉,但原因将在下面的解释中变得显而易见。
为什么您提出的解决方案无效
协变TCov
意味着该方法IInvariant<TCov> M()
可以强制转换到某个IInvariant<TSuper> M()
where TSuper super TCov
,这违反了TInv
in的不变性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>
orIInvariant<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>
.
因此,在执行后一个调用期间的某个时刻,我们隐式地将 an IInvariant<string>
(由实际方法返回)转换为 an 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>
.
因此,在执行后一个调用期间的某个时刻,我们隐式地将 an IInvariant<string>
(这是逆变签名对我们的期望)转换为 an IInvariant<object>
(这是实际方法所期望的)。为此,IInvariant<string>
必须分配给IInvariant<object>
。
概括地说,everyIInvariant<S>
应该可以分配给IInvariant<T>
where S : T
。所以我们再次查看协变类型参数。
现在您可能想知道为什么会出现不匹配。协变和逆变的对偶性哪里去了?它仍然存在,但形式不太明显:
- 当您站在输出端时,引用类型的方差与封闭类型的方差方向相同。由于在这种情况下封闭类型可以是协变或不变的,因此被引用的类型也必须分别是协变或不变的。
- 当您站在输入一边时,引用类型的方差与封闭类型的方差方向相反。由于在这种情况下封闭类型可以是逆变的或不变的,所以引用的类型现在必须分别是协变的或不变的。