原文出自:C#4.0新特性(3):变性 Variance(逆变与协变)
一句话总结:协变让一个粗粒度接口(或委托)可以接收一个更加具体的接口(或委托)作为参数(或返回值);逆变让一个接口(或委托)的参数类型(或返回值)类型更加具体化,也就是参数类型更强,更明确。
通常,协变类型参数可用作委托的返回类型,而逆变类型参数可用作参数类型。对于接口,协变类型参数可用作接口的方法的返回类型,而逆变类型参数可用作接口的方法的参数类型。
协变
我们先来看下面一个来自MSDN的例子:
02 | IEnumerable< string > strings = new List< string >(); |
03 | IEnumerable< object > objects = strings; |
08 | public static void PrintBases(IEnumerable<Base> bases) |
10 | foreach (Base b in bases) |
20 | public static void Main() |
22 | List<Derived> dlist = new List<Derived>(); |
23 | Derived.PrintBases(dlist); |
26 | IEnumerable<Base> bIEnum = dlist; |
下面给协变下个定义:
协变:让一个带有协变参数的泛型接口(或委托)可以接收类型更加精细化,具体化的泛型接口(或委托)作为参数,可以看成OO中多态的一个延伸。
逆变
4 | Action< object > actObject = SetObject; |
5 | Action< string > actString = actObject; |
7 | string strHello(“Hello”); |
大家看到了么?一个声明为Action<object>的类型被赋给了一个Action<string>,大家都知道,Action<T>接收参数,没有返回值,所以其中的object和string是其参数,这个过程其实就是参数的约束更加强了,也就是说让参数类型更加精细化。下面我们来给逆变下个定义:
逆变:让一个带有协变参数的泛型接口(或委托)可以接收粒度更粗的泛型接口或委托作为参数,这个过程实际上是参数类型更加精细化的过程。
一、两个概念:强类型与弱类型
为了后面叙述方便,我现在这里自定义两个概念:强类型和弱类型。在本篇文章中,强类型和弱类型指的是两个具有直接或者间接继承关系的两个类。如果一个类是另一个类的直接或者间接基类,那么它为弱类型,直接或者间接子类为强类型。后续的介绍中会用到的两个类Foo和Bar先定义在这里。Bar继承自Foo。Foo是弱类型,而Bar则是强类型。
有了强类型和弱类型的概念,我们就可以这样的定义协变和逆变:如果类型TBar是基于强类型Bar的类型(比如类型参数为Bar的泛型类型,或者是参数/返回值类型为Bar的委托),而类型TFoo是基于弱类型Foo的类型,协变就是将TBar类型的实例赋值给TFoo类型的变量,而逆变则是将TFoo类型的实例赋值给TBar类型的变量。
二、委托中的协变与逆变的使用
协变和逆变主要体现在两个地方:接口和委托,先来看看在委托中如何使用协变和逆变。现在我们定义了如下一个表示无参函数的泛型委托Function,类型参数为函数返回值的类型。泛型参数之前添加了一个out关键字表示T是一个协变变体。那么在使用过程中,基于强类型的委托Fucntion实例就可以赋值给基于弱类型的委托Fucntion变量。
01 | public delegate T Function< out T>(); |
06 | Function funcBar = new Function(GetInstance); |
07 | Function funcFoo = funcBar; |
10 | static Bar GetInstance() |
接下来介绍逆变委托的用法。下面定义了一个名称为Operate的泛型委托,接受一个具有泛型参数类型的参数。在定义泛型参数前添加了in关键字,表示T是一个基于逆变的变体。由于使用了逆变,我们就可以将基于弱类型的委托Operate实例就可以赋值给基于强类型的委托Operate变量。
01 | public delegate void Operate< in T>(T instance); |
06 | Operate opFoo = new Operate(DoSth); |
07 | Operate opBar = opFoo; |
10 | static void DoSth(Foo foo) |
三、接口中的协变与逆变的使用
接下来我们同样通过一个简单的例子来说明在接口中如何使用协变和逆变。下面定义了一个继承自 IEnumerable接口的IGroup集合类型,和上面一样,泛型参数T之前的out关键字表明这是一个协变。既然是协变,我们就可以将一个基于强类型的委托IGroup实例就可以赋值给基于弱类型的委托IGroup变量。
01 | public interface IGroup< out T> : IEnumerable |
04 | public class Group : List, IGroup |
07 | public delegate void Operate< in T>(T instance); |
13 | IGroup groupOfBar = new Group(); |
14 | IGroup groupOfFoo = groupOfBar; |
下面是一个逆变接口的例子。首先定义了一个IPaintable的接口,里面定义了一个可读写的Color属性,便是实现该接口的类型的对象具有自己的颜色,并可以改变颜色。类型Car实现了该接口。接口IBrush定义了一把刷子,泛型类型需要实现IPaintable接口,in关键字表明这是一个逆变。方法Paint用于将指定的对象粉刷成相应的颜色,表示被粉刷的对象的类型为泛型参数类型。Brush实现了该接口。由于IBrush定义成逆变,我们就可以将基于强类型的委托IBrush实例就可以赋值给基于弱类型的委托IBrush变量。
public interface IPaintable
{
Color Color { get; set; }
}
public class Car : IPaintable
{
public Color Color { get; set; }
}
public interface IBrush<in T> where T : IPaintable
{
void Paint(T objectToPaint, Color color);
}
public class Brush : IBrush where T : IPaintable
{
public void Paint(T objectToPaint, Color color)
{
objectToPaint.Color = color;
}
}
class Program
{
static void Main()
{
IBrush brush = new Brush();
IBrush carBrush = brush;
Car car = new Car();
carBrush.Paint(car, Color.Red);
Console.WriteLine(car.Color.Name);
}
}
四、从Func看协变与逆变的本质
接下来我们来谈谈协变和逆变的本质区别是什么。在这里我们以我们非常熟悉的一个委托Func作为例子,下面给出了该委托的定义。我们可以看到Func定义的两个泛型参数分别属于逆变和协变。具体来说输入参数类型为逆变,返回值类型为协变。
1 | public delegate TResult Func< in T, out TResult>(T arg); |
再重申以下这句话“输入参数类型为逆变,返回值类型为协变”。然后,你再想想为什么逆变用in关键字,而协变用out关键字。这两个不是偶然,实际上我们可以将协变/逆变与输出/输入匹配起来。
我们再从另一个角度来理解协变与逆变。我们知道接口代表一种契约,当一个类型实现一个接口的时候就相当于签署了这份契约,所以必须是实现接口中所有的成员。实际上类型继承也属于一种契约关系,基类定义契约,子类“签署”该契约。对于类型系统来说,接口实现和类型继承本质上是一致的。契约是弱类型,签署这份契约的是强类型。
将契约的观点应用在委托上面,委托实际上定义了一个方法的签名(参数列表和返回值),那么参数和返回值的类型就是契约,现在的关键是谁去履行这份契约。所有参数是外界传入的,所以基于参数的契约履行者来源于外部,也就是被赋值变量的类型,所以被赋值变量类型是强类型。而对于代理本身来说,参数是一种输入,也就是一种采用in关键字表示的逆变。
而对于委托的返回值,这是给外部服务的,是委托自身对外界的一种承诺,所以它自己是契约的履行着,因此它自己应该是强类型。相应地,对于代理本身来说,返回值是一种输出,也就是一种采用out关键字定义的协变。
也正式因为这个原因,对于一个委托,你不能将参数类型定义成成协变,也不能将返回类型定义成逆变。下面两中变体定义方式都是不能通过编译的。
1 | delegate TResult Fucntion< out T, TResult>(T arg); |
2 | delegate TResult Fucntionin TResult>(T arg); |
说到这里,我想有人要问一个问题,既然输入表示逆变,输出表示协变,委托的输出参数应该定义成协变了?非也,实际上输出参数在这里既输出输出,也输出输入(毕竟调用的时候需要指定一个对应类型的对象)。也正是为此,输出参数的类型及不能定义成协变,也不能定义成逆变。所以下面两种变体的定义也是不能通过编译的。
1 | delegate void Action< in T>( out T arg); |
2 | delegate void Action< out T>( out T arg); |
五、逆变实现了“算法”的重用
实际上关系协变和逆变体现出来的编程思想,还有一种我比较推崇的说法,那就是:协变是继承的体现,而逆变体现的则是多态。实际上这与上面分析的契约关系本质上是一致的。
关于逆变,在这里请容我再啰嗦一句:逆变背后蕴藏的编程思想体现出了对算法的重用——我们为基类定义了一套操作,可以自动应用于所有子类的对象。
完整示例
02 | /// 协变和逆变允许数组类型、委托类型和泛型类型参数进行隐式引用转换。 协变保留分配兼容性,逆变与之相反。 |
03 | /// 协变和逆变只能用于引用类型,不能用于值类型或void |
05 | public class CovarianceAndContravariance : IFace |
07 | public CovarianceAndContravariance() |
13 | ///数组的协变允许派生程度更大的类型的数组隐式转换为派生程度更小的类型的数组,但是此操作运行时不是类型安全的操作. |
14 | object [] array = new String[10]; |
18 | Func< object > del = GetString; |
20 | Action< string > del2 = SetObject; |
22 | Action< object > actObject = SetObject; |
23 | Action< string > actString = actObject; |
26 | IEnumerable< string > strings = new List< string >(); |
27 | IEnumerable< object > objects = strings; |
30 | static object GetObject() { return null ; } |
31 | static void SetObject( object obj) { } |
33 | static string GetString() { return "" ; } |
34 | static void SetString( string str) { } |
39 | /// <param name="obj"></param> |
40 | /// <returns></returns> |
41 | public string func( object obj) |
45 | public object func2( string obj) |
50 | public interface IFace |
52 | string func( object obj); |
53 | object func2( string obj); |
一句话总结:协变让一个粗粒度接口(或委托)可以接收一个更加具体的接口(或委托)作为参数(或返回值);逆变让一个接口(或委托)的参数类型(或返回值)类型更加具体化,也就是参数类型更强,更明确。
通常,协变类型参数可用作委托的返回类型,而逆变类型参数可用作参数类型。对于接口,协变类型参数可用作接口的方法的返回类型,而逆变类型参数可用作接口的方法的参数类型。