厘清泛型参数的协变与逆变

协变与逆变(CoVariant and ContraVariant),很多人是糊涂的,我也一直糊涂。其实,对协变与逆变概念糊涂,甚至完全不知道,对一般程序员也没有很大影响。不过,如果你想提高水平,想大概看懂.Net Framework类库中那些泛型接口与泛型类,想大概弄清楚Linq,这个概念还是需要搞清楚。

话又说回来,想弄清楚,其实还是挺费劲的。

如果你还糊涂着这两个概念,相信我,认真看完下面的文字,你会对泛型参数的协变与逆变有一个清晰的理解。

想透彻掌握协变、逆变的概念,首先需要对接口、委托以及泛型等概念有一个相对深入的理解和掌握,否则想理解CoVariant、ContraVariant是不容易的。这些基础知识的掌握,不是这篇随笔的责任,请自己看书。

首先明确几个知识点

(1) 在.Net Framework 4.0中,协变与逆变的概念,应用于泛型的可变类型参数时,只能用于泛型接口和泛型委托(请参考https://msdn.microsoft.com/EN-US/library/dd799517(v=VS.110,d=hv.2).aspx中明确了这一点)。明白这一点,可以将我们的思考范围圈定清楚,使理解和学习更加简单清晰。当然,除了泛型类型参数以外,协变与逆变的概念在其它地方也有应用。

(2) 任何时候,子类向父类转换是安全的,而父类向子类转换,则是不安全的。因为,子类中包含了父类的全部信息,转换为父类,当然没问题;而父类中不包含子类所特有的那些信息,所以,将父类转换为子类是不安全的。当然,如果父类类型的变量中存放的实际上是子类的对象,那么强制类型转换是可以的,这要另当别论。

(3) 再看看msdn中关于协变、逆变以及不变的说法(你可以跳过英文,看我的翻译。):

Covariance and contravariance are terms that refer to the ability to use a less derived (less specific) or more derived type (more specific) than originally specified. Generic type parameters support covariance and contravariance to provide greater flexibility in assigning and using generic types. When you are referring to a type system, covariance, contravariance, and invariance have the following definitions. The examples assume a base class named Base and a derived class named Derived.

  • Covariance

    Enables you to use a more derived type than originally specified.

    You can assign an instance of IEnumerable<Derived> (IEnumerable(Of Derived) in Visual Basic) to a variable of type IEnumerable<Base>.

  • Contravariance

    Enables you to use a more generic (less derived) type than originally specified.

    You can assign an instance of IEnumerable<Base> (IEnumerable(Of Base) in Visual Basic) to a variable of type IEnumerable<Derived>.

  • Invariance

    Means that you can use only the type originally specified; so an invariant generic type parameter is neither covariant nor contravariant.

    You cannot assign an instance of IEnumerable<Base> (IEnumerable(Of Base) in Visual Basic) to a variable of type IEnumerable<Derived> or vice versa.

简单翻一下:

术语协变与逆变是指可以用更泛化(更朝向父类型)或者更具体化(更朝向子类型)的类型替代原有指定类型的能力。泛型参数支持协变与逆变,则为泛型类型的赋值及其使用带来更大的灵活性。当我们讨论的是类型系统时(协变与逆变的概念不止应用于泛型类型系统),协变、逆变与不变分别定义如下。在下面的例子中,Base是父类的类名,Derived是子类的类名。

  • 协变

协变使你可以使用一个更具体的类型替代原始类型。例如,你可以将一个IEnumerable<Derived>的实例赋值给一个IEnumerable<Base>类型的变量。

    本人补充说明:用一个更具体的类型替代原始类型了,那就是说,子类型可以向父类型转换。考虑到子类型对象向父类型对象转换是正常的,就叫协变吧。

  • 逆变

逆变使你可以使用一个更泛化的类型替代原始类型。例如,你可以将一个IEnumerable<Base>的实例赋值给一个IEnumerable<Derived>类型的变量。

    本人补充说明:用一个更泛化的类型替代原始类型了,那就是说,父类型向子类型转换。这和上面的协变相反,叫做逆变。

  • 不变

不变是指你只能使用原始指定的类型;因此,一个“不变”的泛型类型参数既不是协变的也不是逆变的。你不能将一个IEnumerable<Derived>类型的实例赋值给IEnumerable<Base>类型的变量。反过来也不行。

(4) 协变,在C#中使用out关键字表示,逆变使用in关键字表示。

请注意,本篇文章仅限于讨论泛型类型参数的协变与逆变问题,这也是这两个概念用的最多的地方。搞清楚这些,也就基本搞定协变与逆变了。

好了,让我们揭开协变与逆变的神秘面纱

我们前面说了,泛型类型参数的协变与逆变,只用于泛型委托和泛型接口。所以,这很容易枚举穷尽,别想的太复杂,只要我们把泛型委托和泛型接口中的协变与逆变搞清楚了,就等于把协变与逆变搞清楚了。

让我们先看一下委托吧。

泛型委托中的协变与逆变

委托,和C++中的函数指针比较类似。委托是一个引用类型。因此,你可以声明一个委托类型的变量,也可以对这个变量赋值。委托类型的变量存放的是一个方法,包括这个方法的参数和返回值。

先看一段代码:

 1     public class program
 2     {
 3         delegate TResult Fn<in T, out TResult>(T arg);
 4         static void Main()
 5         {
 6             Fn<Object, ArgumentException> fn1 = obj => obj == null ? new ArgumentException() : null;
 7             Fn<string, Exception> fn2;
 8             fn2 = fn1;
 9             Exception e = fn2("123");
10         }
11     }

第3行,首先定义一个委托类型Fn,该委托类型有两个泛型参数,一个是T,用in修饰,是逆变量,另一个是TResult,用out修饰,是协变量。

第6行,声明一个Fn类型的对象(变量)fn1,该委托对象的两个泛型参数的类型分别为object和ArgumentException。fn1使用lambda表达式实现,方法对Object类型的输入参数obj进行检验,若obj为null,则返回一个ArgumentException异常,否则,返回null。该Lambda表达式等价于如下方法:

 1 ArgumentException function(object obj)
 2 {
 3   if(obj == null)
 4   {
 5     return ArgumentException;
 6   }
 7   else
 8   {
 9     return null;
10   }
11 }

第7行,再声明一个Fn类型的对象fn2,该委托对象的两个泛型参数的类型分别为string和Exception。

在第8行,我们直接将fn1赋值给fn2,换句话说,就是将Fn<Object, ArgumentException>类型的变量隐式赋值给了Fn<string, Exception>。

第9行,我们以一个字符串“123”为参数调用fn2方法,该方法返回一个null,指示“123”对象是合法参数对象,不需要抛出异常。

我们来分析一下这里面的协变与逆变。

首先,要清晰地记在心里的一点是,所谓的协变与逆变,都是指“类型”而言,而不是指对象。前面说过了,类型的对象,只能从子类向父类转换,一个实际的父类对象,永远不可能安全转换到子类。所以,别指望说“逆变”能实现父类对象转子类对象的梦想。

fn2=fn1这一句代码里面的协变与逆变,就是类型的协变与逆变,而不是类型的对象的。fn2与fn1都是TResult Fn<in T, out TResult>(T arg)这一委托类型的实例对象。将fn1赋值给fn2,是从fn1向fn2转换。

拿掉泛型之后,fn1及fn2的方法原型分别如下:

fn1: ArgumentException fn1(object);
fn2: Exception fn2(string);

从泛型委托Fn的定义我们可以知道,其返回值为协变,参数值为逆变。这也就是说,Fn的一个对象实例能够向另一个Fn的对象实例转换的条件是:被转换的那个对象实例的返回值的类型必须是更具体化的,而参数值的类型必须是更泛化的。就fn2及fn1而言,ArgumentException类型较Exception类型更具体化,object类型较string类型更泛化。

上述规则为什么存在?更进一步,这些规则为什么是一种合理的存在?不合理的规则,即使存在,也无法执行。

我们先看看赋值语句fn2=fn1的实质是什么。其实质可以从上面的Exception e = fn2("123")语句中看出来。

其实质是:

  (1) 当将fn1赋值给fn2以后,若调用fn2方法,则传递给它的参数及其回值,都是必须要符合fn2的定义的,也就是说,调用时,参数是string,而要求的返回值是Exception;
  (2) 因为fn1被赋值给fn2了,所以,当调用fn2时,被执行的实际上是fn1。因此,实际上被执行的方法要求接收的参数是Object,而返回值是ArgumentException。

综上两点,被执行体要求Object,而传递过来的是string,没问题;被执行体返回了一个ArgumentException类型的返回值,而fn2要求的是Exception类型的值,没问题。因此,上述关于协变与逆变的规则,是合理的,可行的。

看清楚了吧,无论是类型的逆变还是类型的协变,都是要求实例对象(不是类型参数)从子类向父类转换。如果反过来,肯定是编译不通过。

这里插两句闲话,希望不会影响到大家对协变与逆变概念的理解。

(1)在上面例子中,若将delegate Fn定义中的in和out关键字拿掉,则fn2=fn1语句就无法通过编译。为什么呢?我们上面不是已经分析过了,fn1到fn2的转换,是顺理成章的事情,合理且合法,为什么会发生编译错误呢?我的理解是,这就是所谓的“不变”规则在起作用,MSDN明确说了,若泛型委托中的类型变量既不是协变也不是逆变,则其是不可变的,既然不可变,那么这种转换就是要被编译器禁止的。反过来,你若想让他们可变,对不起,请明确使用in或者(和)out关键字把你的想法显式告诉编译器。

(2)in和out关键字在泛型委托中不会有负面作用,只能使你定义的委托适用范围更广。因此,CLR via C#书中明确建议大家尽可能使用in和out关键字来修饰委托的泛型参数。书中原文如下:When using delegates that take generic arguments and return values, it is recommended to always specify the in and out keywords for contravariance and covariance whenever possible, as doing this has no ill effects and enables your delegate to be used in more scenarios.

(3)协变与逆变的概念,不仅应用于泛型类型参数,如下例所示。

 1     delegate Object MyCallback(Circle cir);
 2
 3     string SomeMethod(Shape s)
 4     {
 5         return s.Area.ToString();
 6     }
 7
 8     void TestFn()
 9     {
10         MyCallback callbackFn = SomeMethod;
11     }

例子中,Shape是基类,Circle是其派生类。
第1行定义了一个委托MyCallback,第3行至第6行定义了一个方法SomeMethod,其签名与MyCallback要求的并不一致。但是,从第10行可以看的,我们可以将SomeMethod赋值给MyCallback类型的委托变量。这里,也叫作协变和逆变。

由此可知,协变与逆变的概念,并不仅应用于泛型类型参数。这一对概念在其它地方也有出现。不过,泛型类型参数中的协变与逆变,是这对概念最主要的应用场所。

泛型接口应用中的协变与逆变

如果看明白了上面与泛型委托相关的协变与逆变,那么,泛型接口应用中的协变与逆变就相对容易理解了。

接口到底是什么?

限于本人的水平以及篇幅,这里不能对接口进行系统叙述。仅把本人认为重要的且与协变与逆变相关的一些知识点写在这里。

接口是.Net Framework为了规避多继承所带来的复杂性,又想享受多继承的好处而采取的一种措施。在接口中,只是规定了一些方法(其返回值类型、参数的类型及数量)。接口和抽象基类有类似的地方,但又有很多不同。接口也是一种类型,你可以声明接口类型的变量,也可以对这些变量赋值。一个实现了某一接口的实际类的实例对象,可以被赋值给一个接口变量,隐式。这就好像是说,接口是实现了接口的类的基类一样,将一个子类对象赋值给基类对象,当然是不需要显示转换的。

当我们讨论协变与逆变的时候,可以这样认为:接口,实际上就是一些方法的集合。

如下代码所示:

 1 //先定义一个接口
 2 interface ITest
 3 {
 4     string someMethod(object arg);
 5 }
 6 //再定义一个实现了接口的类  7 class ClassTest : ITest
 8 {
 9     public string someMethod(object arg)
10     {
11         return arg.ToString();
12     }13     public void otherMethod(){...}14 }

有了上面的定义,可以进行如下变量声明:
    ITest objInterface = new ClassTest();

因为ClassTest继承了ITest接口,所以new出来的ClassTest对象,可以隐式赋值给ITest类型的变量。

当我们声明了一个接口类型的变量时,这个变量能调用接口规定的方法,但是,不能调用这个接口变量所代表的实际对象所属的类中,不属于上述接口的其它方法。在上面的例子中,objInterface可以调用someMethod,却不可以调用otherMethod。

再说说泛型接口

泛型接口,就是泛型化后的接口,:)。

泛型接口可以定义泛型类型参数,如:ITestInterface<T>。

与泛型委托类似,也可以对接口的类型参数限定协变与逆变。例如,.Net框架类库中的一个协变泛型接口:

  public interface IEnumerable<out T> : IEnumerable

这个泛型接口有一个类型参数T,这个T是用out修饰的,说明类型T是协变的。为此,我们可以书写如下两行代码:

IEnumerable<String> strings = new List<String>();
IEnumerable<Object> objects = strings;

其中,第一行,声明了一个IEnumerable<string>类型的一个变量strings,用实现了IEnumerable<T>的List<string>类的对象来赋值。第二行,将strings赋值给一个IEnumerable<Object>类型的变量objects。

这看起来好像顺理成章,因为string类型向object类型是可以无忧转换的。所以,IEnumerable<String>向IEnumerable<Object>也是无忧转换?

但是,等一下!

string==>object没问题,是不能说明IEnumerable<String>==>IEnumerable<Object>没问题吧!因为,string派生自object,但是,IEnumerable<String>却并非派生自IEnumerable<Object>,对不啦?一定要搞清楚,无论是Enumerable<String>,还是IEnumerable<Object>,都是派生自Object。他两位之间,是没有派生关系的。

既然没有派生关系,那么,这种转换为什么是可以的呢?

我们前面说过了,接口,实际上就是一些方法的集合。而委托呢?代表的是一个方法。这其中是不是有些联系?对,有联系,甚至,在讨论协变与逆变的概念的时候,接口和委托,实际上可以采用相同的方式去理解。

定义了可变的类型参数的接口,实际上是把这些可变的类型参数传递到接口里面的一个个方法而已,而这些单个的方法,在讨论协变与逆变概念的时候,可以看成委托,没毛病。

说到这里,聪明的你应该不需要我再继续啰嗦了,静下心来想一下,就会明白委托的协变与逆变到底是咋回事了。

泛型接口的类型参数,如上面的T,是传递给接口定义的方法使用的。

定义一个协变的类型参数T,就是告诉接口的方法,T这个类型是协变的,你可以用更朝向父类的类型替代原有类型。在语句IEnumerable<Object> objects = strings中,就是用object类型替换了string类型。一般而言,由于协变的类型参数是朝向父类类型转换,所以可用于接口方法的返回值。你返回一个string给object是没问题的。

同理,定义一个逆变的类型参数,则告诉接口的方法,这个类型是逆变的,你可以用更朝向子类的类型替代原有类型。一般而言,逆变的类型参数,可用于定义方法的参数,而不能用于返回值。这个地方分析起来挺绕人的,就不展开了,参考上面泛型委托中的协变与逆变相关内容的讨论就明白了。

总结

总的来讲,协变的泛型类型参数可用于委托的返回值类型,或者接口中方法的返回值类型。而逆变的泛型类型参数,则可用于委托的参数类型,或者接口的方法的参数类型。注意了,协变和逆变,指的都是类型,而不是对象,这是问题的关键,很多人迷惑,就是迷惑在这里

就说这么多吧,文中有不对的地方,请各位达人批评指正。

时间: 2024-08-05 07:03:16

厘清泛型参数的协变与逆变的相关文章

泛型中的协变和逆变

[泛型中的协变和逆变] 协变指能够使用比原始指定的派生类型的派生程度更大的类型,逆变指能够使用比原始指定的派生类型的派生程度更小的类型. 协变与逆变的本质就是参数的替换.逻辑不变,只进行参数的替换,以实现更高程序的复用. 通常,协变类型参数可用作委托的返回类型,而逆变类型参数可用作参数类型. 对于接口,协变类型参数可用作接口的方法的返回类型,而逆变类型参数可用作接口的方法的参数类型. 协变是out,逆变是in. 协变的例子: 逆变的例子,When the delegate of type Act

Java泛型中的协变和逆变

Java泛型中的协变和逆变 一般我们看Java泛型好像是不支持协变或逆变的,比如前面提到的List<Object>和List<String>之间是不可变的.但当我们在Java泛型中引入通配符这个概念的时候,Java 其实是支持协变和逆变的. 看下面几行代码: // 不可变 List<Fruit>fruits =newArrayList<Apple>();// 编译不通过 // 协变 List<?extendsFruit>wildcardFruit

协变、逆变与不变:数组、泛型、与返回类型

转自:http://blog.csdn.net/yi_Afly/article/details/52071260 1. 前言 之前几篇博文,有些地方涉及到了协变性.逆变性与不变性在Java中的表现,所以这篇博文将重点记录这方面的内容,并辅以JDK源码中的一些实例,加以说明. 2. 定义 这里讨论的协变.逆变与不变都是编程语言中的概念.下面介绍定义: 若类A是类B的子类,则记作A ≦ B.设有变换f(),若: 当A ≦ B时,有f(A)≦ f(B),则称变换f()具有协变性. 当A ≦ B时,有f

泛型的协变和逆变

转载C# 泛型的协变和逆变 1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用.如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量.协变和逆变是两个相互对立的概念: 如果某个返回的类型可以由其派生类型替换,那么这个类型就是支持协变的 如果某个参数类型可以由其基类替换,那么这个类型就是支持逆变的. 2. C# 4.0对泛型可变性的支持 在C# 4.0之前,所有的泛型类型都是不变量——即不支持将一个泛型类型替换为另一个泛型类型,即使它们之间

面向对象设计——协变与逆变

在面向对象的设计中,我们一直追求一种结果,就是良好的复用性,基于这个理念,面向对象的设计中加入了协变与逆变(Covariance and Contravariance)两个概念,我们先来简单了解一下这两个概念. 简介: 协变:由子类向父类方向转变, 用out关键字标识 逆变:由父类向子类方向转变, 用in关键字 举例:Animal是父类,Dog是从Animal继承的子类:如果一个对象的类型是Dog,那么他必然是Animal.有一个获取宠物的方法要接受Dog参数,那么另一个接受Animal参数的方

C#4.0中的协变和逆变

原文地址 谈谈.Net中的协变和逆变 关于协变和逆变要从面向对象继承说起.继承关系是指子类和父类之间的关系:子类从父类继承所以子类的实例也就是父类的实例.比如说Animal是父类,Dog是从Animal继承的子类:如果一个对象的类型是Dog,那么他必然是Animal. 协变逆变正是利用继承关系 对不同参数类型或返回值类型 的委托或者泛型接口之间做转变.我承认这句话很绕,如果你也觉得绕不妨往下看看. 如果一个方法要接受Dog参数,那么另一个接受Animal参数的方法肯定也可以接受这个方法的参数,这

[C#]浅谈协变与逆变

看过几篇说协变与逆变的博客,虽然都是正确无误的,但是感觉都没有说得清晰明了,没有切中要害.那么我也试着从我的理解角度来谈一谈协变与逆变吧. 什么是协变与逆变 MSDN的解释:https://msdn.microsoft.com/zh-cn/library/dd799517.aspx 协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型.泛型类型参数支持协变和逆变,可在分配和使用泛型类型方面提供

c# 协变和逆变

由子类向父类方向转变是协变,用out关键字标识,由父类向子类方向转变是逆变,用in关键字 协变和逆变的应用 一. 数组的协变 Animal[] animalArray = new Dog[]{}; 说明:声明的数组数据类型是Animal,而实际上赋值时给的是Dog数组:每一个Dog对象都可以安全的转变为Animal.Dog向Animal方法转变是沿着继承链向上转变的所以是协变 二. 委托中的协变和逆变 1.委托中的协变   C# 代码   复制 //委托定义的返回值是Animal类型是父类 pu

【转】c# 协变和逆变

本文转自:http://www.cnblogs.com/rr163/p/4047404.html C#的协变和逆变 由子类向父类方向转变是协变,用out关键字标识,由父类向子类方向转变是逆变,用in关键字 协变和逆变的应用 一. 数组的协变 Animal[] animalArray = new Dog[]{}; 说明:声明的数组数据类型是Animal,而实际上赋值时给的是Dog数组:每一个Dog对象都可以安全的转变为Animal.Dog向Animal方法转变是沿着继承链向上转变的所以是协变 二.