C#中逆变与协变一般应用到以下三种场景
一、数组的协变
数组的协变允许派生程度更大的类型的数组隐式转换为派生程度更小的类型的数组。
比如:Object[] os = new String[5];
二、泛型接口的逆变与协变
public class Program
{
static void Main(string[] args)
{
I<C2, C2> i = new U<C1, C3>();
}
}
class C1{}
class C2:C1{}
class C3:C2{}
interface I<in P, out R>
{
R Func(P p);
}
class U<P, R> : I< P, R>
{
public R Func(P p)
{
throw new NotImplementedException();
}
}
上面的代码中,接口I同时应用了逆变和协变。
先说逆变:逆变允许 方法具有 与接口的泛型类型参数(实例中的 in P)所定义的参数类型(P作为Func的参数类型)相比,派生程度更大的返回类型。
在Main函数中,我们给定泛型接口I的类型参数中的R为C2,但实际的实现却是C1,并正常运行。为什么会发生这种情况呢?因为,
我们给定的参数类型是C2,也就是说变量i在其它调用者的眼里,含有一个参数类型为C2的方法Func,但i的骨子里却是用C1实现的,这样有何不可呢?
客户代码提供给i.Func的参数的类型是C2,而C2可以隐式转化为C1,一点也不影响使用。
再说协变,同样的道理,Func的返回值肯定是要用在适和C2类型的变量的地方,那么i.Func提供给客户代码一个C3类型的变量,又何乐不为呢?
三、委托的逆变和协变
对于普通委托:例如,
delegate C2 F1(C2 c);
F1 f1 = new U<C1, C3>().Func;
不需要说明什么,可以很自然地应用逆变与协变
但是对于泛型委托,例如,
delegate R F2<in P,out R>(P p);
F2<C2, C2> f2 = new U<C1, C3>().Func;
可以使用in和out关键字指定类型参数的逆变或协变,也可以不使用,两者没有什么区别。但需要注意的是,要么使用正确,要么不使用,否则会出现编译错误。
正确的作法是,在方法中作为参数的类型逆变,使用in关键字,作为返回值的类型协变,使用out关键字。
另外,还需要注意的是,当委托规定方法的参数有ref和out修饰时,该参数无法应用逆变,比如:
delegate C2 F0(ref C2 c);
F0 f0 = new U<C1, C3>().Func;//编译错误