当我们有一个继承BBase的基类和一个专门处理它的派生类时,假设有一个委托需要Base
作为输入。
using System;
class BBase {}
class Base : BBase {}
class Derived : Base {}
delegate void BaseDelegate(Base b);
在使用委托时,不允许使用BaseDelegate b2 = TakeDerived;
,因为输入是反变量的。
class MainClass
{
static void TakeBBase(BBase bb) {}
static void TakeBase(Base b) {}
static void TakeDerived(Derived d) {}
static void Main(string[] args)
{
BaseDelegate b1 = TakeBase;
b1(new Derived());
b1(new Base());
// ERROR
// parameters do not match delegate
// `BaseDelegate(Base)' parameters
// The contract of b2 is to expect only Base
//BaseDelegate b2 = TakeDerived;
TakeBBase可以分配给BaseDelegate。
BaseDelegate b2 = TakeBBase;
b2(new Derived());
b2(new Base());
同样有趣的是,我们可以将基类的子类分配给委托中的Base类型参数。协方差/反方差规则在前面的例子中似乎不起作用。
- 为什么C#选择在委托中的输入参数中使用反方差(而不是协方差)
- 当协变/逆变规则在C#中工作时?除代表外,还有哪些其他情况使用协方差/反方差?为什么
Olivier的回答是正确的;我想我可以试着更直观地解释一下。
为什么C#选择在委托中的输入参数中使用反方差(而不是协方差)?
因为反方差是类型安全的,所以协方差不是。
用Mammal:代替Base
delegate void MammalDelegate(Mammal m);
这意味着";一个取哺乳动物而不返回任何东西的函数";。
所以,假设我们有
void M(Giraffe x)
我们可以用它作为哺乳动物代表吗?不可以。哺乳动物代表必须能够接受任何哺乳动物,但M不接受猫,它只接受长颈鹿。
void N(Animal x)
我们可以用它作为哺乳动物代表吗?对哺乳动物代表必须能够接受任何哺乳动物,N接受任何哺乳动物。
协方差/反方差规则在这个例子中似乎不起作用。
这里没有差异。您犯了一个极其常见的错误,将分配兼容性与协方差混淆。分配兼容性是而不是协方差协方差是类型系统转换保持赋值兼容性的特性。
让我再说一遍。
你有一个方法,需要一个哺乳动物。你可以把它传给长颈鹿这不是协方差。这就是分配兼容性。该方法具有Mammal类型的形式参数。这是一个变量。你有长颈鹿式的价值观。该值可以分配给该变量,因此它与赋值兼容。
如果不是分配兼容性,那么方差是什么?让我们来看一两个例子:
长颈鹿是一种与哺乳动物类型相适应的动物。因此,长颈鹿序列(IEnumerable<Giraffe>
)与哺乳动物类型序列(IEnumerable<Mammal>
)的一个变量具有分配相容性。
这是协方差。协方差是事实上,我们可以从另外两个类型的赋值兼容性中推断出两种类型的赋值相容性。我们知道长颈鹿可能被分配给一种可变类型的动物;这让我们可以推断出关于另外两种类型的另一个赋值兼容性事实。
您的代表示例:
哺乳动物是与类型动物的变量相容的分配。因此,采用动物的方法与采用哺乳动物类型委托的变量是赋值兼容的。
这就是方差。相反,我们可以从另外两个类型的赋值兼容性中推断出两个事物的赋值兼容性——在这种情况下,一个方法可以被赋值给一个特定类型的变量。
协方差和反方差之间的差异简单地是;方向";被交换。利用协方差,我们知道A can be used as B
意味着I<A> can be used as I<B>
。与方差相反,我们知道I<B> can be used as I<A>
。
同样:方差是关于在类型转换中保持赋值兼容性关系的事实。子类型的实例可以分配给其超类型的变量,这不是事实。
除委托外,还有哪些其他情况使用协方差/反方差,为什么?
方法组到委托的转换在返回和参数类型上使用协方差和反方差。只有当返回/参数类型是引用类型时,这才有效。
泛型委托和接口在其类型参数中可以标记为协变或逆变;编译器将验证方差是否始终是类型安全的,如果不是,则不允许方差注释。只有当类型参数是引用类型时,这才有效。
元素类型为引用类型的数组是协变的;这不是类型安全的,但它是合法的。也就是说,你可以在任何需要
Animal[]
的地方使用Giraffe[]
,即使你可以把乌龟放进一组动物中,但不能放进长颈鹿中。尽量避免那样做。
请注意,C#不支持虚拟函数返回类型协方差。也就是说,您可以不制作基类方法virtual Animal M()
,然后在派生类override Giraffe M()
中制作。C++允许这样做,但C#不允许。
关于上一段的更新:此答案写于2016年;在2020年,C#9现在支持返回类型协方差。
因为,如果您提供一个接受派生程度较低的输入参数的委托,则此方法将获得一个类型比预期派生程度更高的参数值。这是有效的。
另一方面,如果使用协方差,则可以提供一个期望派生类型更高的委托,但它可能会得到派生类型更低的值。这是行不通的。
BaseDelegate b = TakeBBase; // Contravariant. OK.
b(new Base());
因为b
被静态声明为BaseDelegate
,所以它接受类型为Base
的值或从中派生的类型。现在,因为b
实际上正在调用TakeBBase
,所以它将这个Base
值传递到期望BBase
值的地方。由于Base
是从BBase
派生而来的,所以这是可以的
BaseDelegate b = TakeDerived; // Covariant. DOES NOT COMPILE!
b(new Base());
现在正在调用TakeDerived
,并且正在获取类型为Base
的值,但正在期待类型为Derived
的值,而Base
显然不是。因此协方差不是类型安全的。
注意:对于输出参数,注意事项恰恰相反。因此,out
参数和返回值是协变的。
让它有点违反直觉的是,我们谈论的不仅仅是或多或少派生的值,而是委托接受(或返回)或多或少衍生的值。
相应的参数应用于泛型类型参数。在这里,您提供了或多或少具有方法的派生类型,对于这些方法(包括属性getter和setter),这与您的委托一样有问题。