SUMTEC -- There's a thing in my bloglet.

But it's not only one. It's many. It's the same as other things but it exactly likes nothing else...

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  263 随笔 :: 19 文章 :: 3009 评论 :: 74万 阅读
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

前言

今天看了两篇讲协变/反变的文章,写得很好也很有意思。不过我猜应该有不少人可能还是很难理解这个新概念——每一次推出新的概念的时候,都会或多或少造成我们的困惑:这是个什么东西?为什么要出这么复杂的东西?我们什么时候应该用这种东西,什么时候不该用?

有这样的困惑没关系,我想绝大多数人都经历过这个过程。我在这里呢,也说说从我的角度是如何看这个新鲜事物的,也许对理解这个东西有帮助。不过先声明一下,我没有装过,更没有用过.NET 4.0,因此我写的内容基本是自己推导出来的,如果有什么不正确的地方,也希望大家能够指出。

先给出刚才提到的两篇文章,因为也许有人是通过搜索引擎过来的:

http://www.cnblogs.com/Hush/archive/2008/11/22/1339140.html

http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html

其中第一篇文章的内容相对简单一点,也好理解一点,后者更加理论化一些。
本来呢,我看完Ninputer的文章产生了两个想法:
1、好!
2、有点太理论化了,恐怕难以理解。
基于第二点想法,我就产生了要从更容易理解的角度去描述这个问题的想法,结果一刷屏幕,已经出来了第二篇了——有人捷足先登了。这让我郁闷了一下会儿,不过后来还是发现了一些有趣的事情大家都没有提到,于是又重燃了我码一堆文字的热情。

 


什么是协变和反变?

其实前面的文章里面已经有很清晰和正规的定义,我这里不打算再写得更详细了。相反,我想把问题简化,因此我会给出一个较简单但不太准确的说法:

interface IFoo<in TIn, out TOut> // TIn 就是反变,TOut就是协变
{
    TOut Output();
    
void Intput(TIn value);
}

 

这个定义估计足够简单明了了,那么他们是干什么用的呢?

提示:理解协变和反变,需要从泛型之间的类型转换入手,而不要把注意力定格在泛型中某个函数的类型参数T上面。比如:IEnumerable<object> objs = new List<string>();

 

 

为什么要有协变和反变?

这个问题嘛,跟继承和派生的概念是有一定关联的。比方说:

对派生和继承了解的就别看了,浪费时间!

这个和协变好像还有点远,哦,对,还跟泛型有关系:我们在泛类型上是否也可以像刚才那样使用呢?我们看一个例子:

IList<string> source = new List<string>;
IList
<object> target = source;  // 可以这么干吗?

我们先撇开“能不能”不说,至少我们是很期望能够这么干的,比如说:

复制代码
public void RemoveNull(IList<object> objList)
{
  
// 把中间员素值为null的元素删掉
}

IList
<string> stringList = GetItFromSomewhere();
RemoveNull(stringList); 
// 嗯,看着很诱人的样子!
// 如果这样做是被允许的话,那么我们就不用再写一个针对
// IList<string>版本的RemoveNull函数了
复制代码

那到底能不能呢?这个问题很有趣,在Ninputer的文章里面提到了另外一个类似的例子:

如果两个类型T和U之间存在一种安全的隐式转换,那么对应的数组类型T[]和U[]之间是否也存在这种转换呢?
……
举个例子,就是String类型继承自Object类型,所以任何String的引用都可以安全地转换为Object引用。我们发现String[]数组类型的引用也继承了这种转换能力,它可以转换成Object[]数组类型的引用,…… 

我这里只是节选了其中一部分,是因为有一部分的描述是不准确的。为什么说是不准确的呢?我们来看这么一段例子:

string[] source = { "A""B""C" };
object[] target = source; // 编译会出错吗?不会!
target[1= 1// 能运行通过吗?不能!

由于编译是可以通过的,所以上面我节选的那一段话是正确的。但由于实际上运行是不通过的,原因也很显而易见。

关于Ninputer的原话,以及不那么显而易见的“显而易见”的解释可以看这里

 

刚才的object数组的例子,能够给我们带来很多的思考:

一、为什么数组就可以编译通过,那么泛型呢?

泛型和数组的对比

泛型的类型转换是不能编译通过的!可为什么不行呢?其实前面数组的那个例子已经给出了答案:运行的时候如果我们试图对某个元素做赋值操作,是有可能出现运行时错误的。实际上数组本身也没有解决这个问题,只是忽略了这个问题。忽略这个问题的原因也很简单,比如说我们看看string.Format(string format, params object[] objs)这个函数,如果不忽略又怎么提供这种方法呢?可以说数组允许这种情况的转换,其实是一种不得不作出的妥协,而并不是真正的协变(按照泛型的协变/反变规则,其实是不允许这么做的)。


二、如果我们想要编译及运行通过,应该怎么去做?

前面曾经举了一个例子,说明我们是那么期望这种泛型之间的转换,协变和反变就是为了解决这一问题的。让我们回顾最开始的那个例子:

interface IFoo<in TIn, out TOut> // TIn 就是反变,TOut就是协变
{
    TOut Output();
    
void Intput(TIn value);
}

我们考虑有如下的代码:

Code

通过这段代码,应该能理解,如果我们需要在泛型之间能够达成安全的隐式类型转换,是会有一定的前提条件限制的。在应用这些限制之前,泛型之间的隐式转换是不可能的事情,即使类型参数T之间是有继承关系的。而协变/反变就是为了完成泛型间类型转换而提供的,用于明确限制转换方向、给编译器验证条件的语法工具。

 

什么时候使用协变/反变?

我的经验是,先看看框架里面是怎么用的,用多了之后再总结,别轻易在自己设计的泛型中使用。原因很简单:刚学会新特性的时候,很容易把它当作金锤子四处滥用,最后可能反而会增加了整个程序的复杂度。 

 

后记

大家考虑一下,前面举的例子IList<T>能通过协变/反变来达到目的吗?答案在Ninputer的文章中。

posted on   Sumtec  阅读(2404)  评论(16编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
点击右上角即可分享
微信分享提示