理想与现实之间

学习的最好方法就是blog

博客园 首页 新随笔 联系 订阅 管理

按:这文章算是上星期与装配脑袋一起讨论到的一些东西的总结。我试图用更多一点的代码把协变和反变解释得更浅显一点。大家也可以参考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  阅读(2706)  评论(13编辑  收藏  举报