[.Net 4.0]泛型的协变,以及高阶函数对泛型的影响 Part 1

按:这文章算是上星期与装配脑袋一起讨论到的一些东西的总结。我试图用更多一点的代码把协变和反变解释得更浅显一点。大家也可以参考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用在参数的时候,可以反变,用作返回类型的时候,可以协变。当又是参数又是返回类型的时候,就既不能协变也不能反变了。在通常的应用中,这是正确的。这里所谓的通常,是指没有高阶函数存在的情况。很不幸的是,其实我们还蛮容易就会碰到有高阶函数的情况。那么具体的内容,让我们在下一篇里再继续。

 

你可能感兴趣的:([.Net 4.0]泛型的协变,以及高阶函数对泛型的影响 Part 1)