最近花一周时间阅读了《c#图解教程》的前20章,由于有一定语言基础且这本书讲解的十分通俗易懂,大部分章节都看一遍就懂了。唯独18章最后的18.10节第一遍没有看明白,反复读了两遍这一节内容后,我终于明白了c#中的协变与逆变的概念。下面讲一讲我的理解,尽量通俗易懂的为初学者解释清楚。
如果你已经看懂了《c#图解教程》第18章18.10节,那没必要再继续看了。但如果看书觉得云里雾里,或是c#初学者想弄懂这两个概念,不妨读一读此文。
我的经验是,看文字对于协变和逆变的说明很容易一头雾水,最好是先看懂两段代码分别是怎么回事,以及代码中问题的点在哪。然后重点分析两个Main函数的最后一行,通过委托执行函数时发生了什么以及为什么能够成功。顺着代码把运行的过程搞懂了,基本也就明白了协变和逆变。
好了,下面正式开始讲解:
c#中的可变性包括三种——协变,逆变与不变。可变性处理的是可以使用基类型替换派生类型的安全情况。这句话先记住!无论协变还是逆变,都是用于应对 使用基类型替换派生类型的安全情况 !记牢了这句话,后面理解什么情况下是协变、什么情况下是逆变就容易很多了。
先说什么是协变:
给定一个泛型委托"delegate T Fun1<out T>();", 协变就是:首先,将派生类类型作为泛型委托的类型参数 并产生一个委托实例。然后,把这个委托实例赋值给一个 由基类类型作为类型参数的委托对象。但要求是,这个前后产生“冲突”的类型参数T,需要在泛型委托声明时加上“out”标识,且这个T只能处于返回和输出的位置(不能出现在Fun1的参数列表中)。
这么说一定很糊涂,我们看一下简单的例子:
using System;
namespace ConsoleApp1
{
//定以委托类型,注意out标识
delegate T Fun1<out T>();
//定义基类型
class BaseClass { }
//定义派生类
class DerivedClass : BaseClass { }
class Programe
{
//定义方法
static DerivedClass Function1()
{
return new DerivedClass();
}
static void Main()
{
//通过基类类型作为模板参数定义委托实例,Function1的返回类型是DerivedClass
Fun1<DerivedClass> del1 = Function1;
//协变机制!
//del1是Fun1<DerivedClass>类型的委托对象,但del2属于Fun1<BaseClass>类型的委托对象
Fun1<BaseClass> del2 = del1;
//执行
BaseClass baseCase = del2();
}
}
}
在通过del2()执行时,del2所属的委托类型是“delegate BaseType Fun1<out BaseType>();”, 返回的是一个BaseType类型的变量,而实际调用的函数是“ static DerivedClass Function1()”,虽然返回的是DerivedClass类型,但由于DerivedClass派生自BaseClass,将派生类对象赋值给集类对象并无问题,这就是协变。回到前面说的,“可变性处理的是可以使用基类型替换派生类型的安全情况”。协变所处理的这种情况是不是属于安全的替换?
再说什么是逆变:
定义一个委托类型“delegate void Fun2<in T>(T t);”, 与协变类似,逆变是首先将派生类类型作为泛型委托的类型参数 并产生一个委托实例,然后把这个委托实例赋值给一个 由基类类型作为类型参数的委托对象。
但与协变不同之处在于逆变中的逆变参数T加in标识符,同时只能出现在参数列表中。说的不清楚,还是看代码直观:
using System;
namespace ConsoleApp2
{
delegate void Fun2<in T>(T t);
//定义基类型
class BaseClass{ }
//定义派生类
class DerivedClass : BaseClass { }
class Program
{
//定义方法(省略中间class的定义)
static void Function2(BaseClass elem) { }
static void Main()
{
//通过基类类型作为模板参数定义委托实例
Fun2<BaseClass> del1 = Function2;
//逆变机制!
//del1是Fun2<BaseClass>类型的委托对象,但del2属于Fun2<DerivedClass>类型的委托对象
Fun2<DerivedClass> del2 = del1;
DerivedClass derivedObj = new DerivedClass();
//执行
del2(derivedObj);
}
}
}
执行del2时,由于del2属于派生类DerivedClass作为模板参数而产生的委托对象,属于"elegate void Fun2<DerivedClass>(DerivedClass t);"委托类型对应的委托实例,调用它的时候自然要传入DerivedClass类型的参数。但真正调用的函数是void Function2(BaseClass elem),所需要的参数类型是BaseClass,但由于DerivedClass是BaseClass的派生类,自然也可以安全的转换。
以上就是我对于c#中协变与逆变的理解,举的例子都是针对泛型委托的,其实对于泛型接口也大同小异,两段代码亲测均能通过visual studio运行成功。
欢迎大家讨论、提出自己的意见。我觉得这是《c#图解教程》前20章中最值得思考的一节内容~
自己开始写文章才发现,把一个自己理解的问题通过文章的方式讲清楚还是有点困难的。。。。