为什么类类型参数的方差必须与其方法的返回/参数类型参数匹配?
并不是这样!
返回类型和参数类型不需要匹配封闭类型的差异。在您的示例中,它们需要对两个封闭类型都是协变的。这听起来有些违反直觉,但原因将在下面的解释中变得显而易见。
为什么你提出的解决方案不可行
协变的 TCov
意味着方法 IInvariant<TCov> M()
可以被转换为一些 IInvariant<TSuper> M()
,其中 TSuper super TCov
,这违反了 IInvariant
中 TInv
的不变性。然而,这种暗示似乎并不必要:通过禁止转换 M
,可以轻松地强制执行 IInvariant
对 TInv
的不变性。
- 你所说的是,具有变体类型参数的通用类型可以分配给同一通用类型定义和不同类型参数的另一个类型。这部分是正确的。
- 但你还说,为了解决潜在的子类型化违规问题,在此过程中方法的显式签名不应更改。这是不正确的!
例如,
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>
。
为了概括,每个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
。因此,我们再次看到了一个协变类型参数。
现在你可能会想知道为什么存在不匹配。协变和逆变的二元性去哪了?它仍然存在,但以一种不太明显的形式出现:
• 当你处于输出侧时,引用类型的方差与封闭类型的方差方向相同。由于在这种情况下封闭类型可以是协变或不变的,因此引用类型必须分别是协变或不变的。
• 当你处于输入侧时,引用类型的方差与封闭类型的方差方向相反。由于在这种情况下封闭类型可以是逆变或不变的,因此引用类型现在必须是协变或不变的。