理想与现实之间

学习的最好方法就是blog

博客园 首页 新随笔 联系 订阅 管理
  68 Posts :: 0 Stories :: 419 Comments :: 12 Trackbacks

按:这文章算是上星期与装配脑袋一起讨论到的一些东西的总结。我试图用更多一点的代码把协变和反变解释得更浅显一点。大家也可以参考Ninputer同学的文章:

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

 

为什么要有协变

首先来说明一下为什么会要协变。协变其实是一个相当简单的概念。我们知道在OO的语言中,可以把一个子类的实例赋值给一个基类的引用。就像这样:

FileStream fs = new FileStream();
Stream s 
= fs;

 

当引入了泛型之后,很容易我们会想到,像下面这样一个赋值是不是可以呢:

interface ISample<T> {}
class ConcreteFileStream : ISample<FileStream>
{}

ISample
<FileStream> iFs = new ConcreteFileStream();
ISample
<Stream> iS = iFs; //这里的赋值是可以的吗?

 

直观上来看FileStream是Stream的子类,那么这个赋值当然应该是可行的。如果是可行的,那么我们称ISample<T>的泛型参数T,支持协变。

然而事实上,情况远没有这么简单。协变并不是像我们主观上认为的那样,总是可以的。在某些情况,泛型参数不能进行协变,但可以进行反变。所谓的反变,就是指对于这个泛型参数,我们可以做如下的赋值:

class ConcreteStream : ISample<Stream>
{}

ISample
<Stream> iS = new ConcreteStream();
ISample
<FileStream> iFs = iS; //如果这样的赋值可以进行,则称这个泛型参数支持反变

 

这样的赋值竟然是可以的?是不是有违我们的直觉?接下来我们讨论一下什么情况下可以协变,什么情况下可以反变,而什么情况下两者都不可以。

这里需要说明的一点是,我们这里说“一个赋值可以进行”,意思是指这个赋值不会引发类型不安全的形为。不会因此,导致类型相关的异常。在实际使用中,C#编译器会检测到“这样一个赋值是不可以进行的”,从而会引发问题的代码不能通过编译。而泛型参数也要显示的声明成是否可以协变与反变。

 

什么时候可以协变与反变?

之前提到了,要进行协变,远比我们的直觉要复杂的多。一个泛型参数是不是可以协变,取决于接口所定义的方法是如何使用这个泛型参数的。让我们来看下面的例子:

interface ISample<T>
{
    T foo();
}

 

此时,T是被用作返回值的类型。而在这种情况下,T是支持协变的。请看以下的代码:

ISample<FileStream> iFs = new ConcreteFileStream();

ISample
<Stream> iS = iFs;
Stream s 
= iS.foo(); //iS.foo()返回FileStream对象,可以隐式转化为Stream类型,没有问题!!

 

当我们使用ISample<Stream>的时候,因为类型参数为Stream,所以代码中我们指望foo方法会返回一个Stream对象。而当iS实际指向一个ISample<FileStream>的对象时,foo方法会返回一个FileStream对象。而因为FileStream是Stream的子类,因此也是一个Stream对象。所以,在这里这个赋值不会引发任何问题。

在C# 4.0当中,如果一个泛型参数可以进行协变,我们要显示地进行声明,最通常的,当T被传出的时候,可以进行协变(这在有高阶函数的时候不成立,但我们稍后再讨论),所以我们要用一个out关键字来修饰T,说明它可以协变,像这样:

interface ISample<out T>
{}

 

接下来让我们看一个不能协变的例子:

interface ISample<T>
{
    
void foo(T t);
}

ISample
<FileStream> iFs = new ConcreteFileStream();

ISample
<Stream> iS = iFs; //如果可以,会引发以下的情况,导致类型不安全

Stream s 
= new Stream();
iS.foo(s); 
//不能把s转化为FileStream!!!

 

 

 我们可以看到当使用iS的时候,我们认为类型参数是Stream,因此调用foo的时候,我们可能会把一个Stream类型的对象当作参数传递给foo。而当iS实际指向一个ISample<FileStream>的时候,foo函数要求的是一个FileStream对象。而Stream是不能转化为FileStream的。所以如果允许这样的赋值,在运行时会有一个InvalidCastException。实际情况是,C#编译器会检测到这段代码的问题,不会让代码通过编译。而在这种情况下,iS不可以指向ISample<FileStream>,也就是说T不支持协变。比较有趣的是,在这种情况下,T可以进行反变:

ISample<Stream> iS = new ConcreteStream();

ISample
<FileStream> iFs = iS;
FileStream fs 
= new FileStream();
iFs.foo(fs);
//可以将fs转化为Stream类型,所以不会有类型不安全!

 

 

在这里,使用iFs的时候,因为T的类型被指定为FileStream,foo的名义签名为 void foo(FileStream t),所以我们有可能将一个FileStream类型的对象传递给foo函数。而当iFs实际指向一个ISample<Stream>时,foo的实际签名为void foo(Stream t),而当我们把fs传递给foo的时候,会发生一个从FileStream到Stream的转换。FileStream是子类,所以这个转换完全可行。在这里,我们把一个ISample<Stream>赋值给一个ISample<FileStream>,而不会引发类型不安全。这种情况,我们称T支持反变。我们要用in关键字来显示地说明这一点:

interface ISample<in T>
{
    
void foo(T t);
}

 

所以,协变与反变的一般定义如下:

class Base {}
class Derived : Base {}
interface ISample<T> {}

ISample
<Base> iB;
ISample
<Derived> iD;

iB 
= iD; //如果这个赋值是类型安全的,那么T可以协变
iD = IB; //如果这个赋值是类型安全的,那么T可以反变

 

写到这里,你大概会觉得,当T用在参数的时候,可以反变,用作返回类型的时候,可以协变。当又是参数又是返回类型的时候,就既不能协变也不能反变了。在通常的应用中,这是正确的。这里所谓的通常,是指没有高阶函数存在的情况。很不幸的是,其实我们还蛮容易就会碰到有高阶函数的情况。那么具体的内容,让我们在下一篇里再继续。

 

posted on 2008-11-22 23:00 Justin Shen 阅读(1641) 评论(13)  编辑 收藏 网摘 所属分类: 技术随想

Feedback

#1楼 2008-11-22 23:09 devil0153      
LZ写的不错
不过4.0搞的有点四不象了:-(

  回复  引用  查看    

#2楼 2008-11-22 23:16 xiaopohai_long      
呵呵,不错。。。总感觉有点麻烦了。限制太多了。。
  回复  引用  查看    

#3楼 2008-11-22 23:21 wd_Terry      
今天连续看了两篇关于协变的文章,前面装配脑袋那篇写的非常不错,但lz这篇跟易懂,赞一个!期待你的下篇。。。
  回复  引用  查看    

#4楼 2008-11-22 23:47 Gray Zhang      
那么原有的泛型类有修改过么,List<FileStream>可以当List<Stream>用了吗?
  回复  引用  查看    

有没有人性啊,我刚坐了16个小时的飞机居然让我看这种东西
  回复  引用    

#6楼[楼主] 2008-11-23 00:03 Justin Shen      
回Gray:
不可以,如果没有in和out的修饰,仍然是不可以进行协变的。这是编译器的规定,为了提高对现有代码的兼容。有时间的话,我会在后续的文章里解释一下原因。

  回复  引用  查看    

#7楼 2008-11-23 01:11 Q.Lee.lulu      
和原来2.0的协变、逆变有什么区别 ?
  回复  引用  查看    

#8楼 2008-11-23 02:21 Jeffrey Zhao      
List<T>,不像IComparable<T>或IEnumerable<T>,也没法加上in或out。
  回复  引用  查看    

#9楼[楼主] 2008-11-23 12:01 Justin Shen      
2.0里面只支持delegate参数的协变,但不支持泛型参数的协变。
  回复  引用  查看    

#10楼 2008-11-23 12:26 装配脑袋      
支持一下,我就是懒得举这些具体例子,LZ太耐心了^_^
  回复  引用  查看    

#11楼 2008-11-23 16:39 FZZZ[未注册用户]
我关心的是Net的Api封装进程(互操作太麻烦了有时狠不得直接用C++)和它的性能,语法糖我是不关心的,也很害人的
  回复  引用    

--引用--------------------------------------------------
FZZZ: 我关心的是Net的Api封装进程(互操作太麻烦了有时狠不得直接用C++)和它的性能,语法糖我是不关心的,也很害人的
--------------------------------------------------------


我得很遗憾的说,你到现在也不知道什么是真正的底层。我用Linq书写特定算法,可以比C++快10-100倍。而且你不要以为Linq是语法糖,所以可以不用,告诉你就是因为Linq所以我才能写出快100倍的算法。你信吗?

  回复  引用    

#13楼 2008-11-24 09:52 GUO Xingwang      
楼主写得很清晰,懂了
  回复  引用  查看    

发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 1339140




历史上的今天:
2005-11-22 有多少东西需要学习?

相关文章:

相关链接: