本文发表于CodeProject,原文地址http://www.codeproject.com/Tips/631360/Covariance-in-Csharp
关于协变和逆变,园子里也有篇大牛的文章:http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html
如果提起Covariance这个单词,我们也许能把它和数学或者静态学联系起来(Covariance在数学中可以被翻译成协方差,静态学中被译为共变)。它在维基百科中被解释为:在统计学或者静态学中,Covariance用来衡量两个随机变量一同变化的程度。
根据多态性,我们可以构建如下代码:
BaseClass objbaseClass = new DeriveClass();
假设上边代码是成立的,那么下边的代码呢?
List<BaseClass> listObjbaseClass =new List<DeriveClass>(); //会出现编译错误
如果a=b,则可以推导出a+a=b+b,或者List<a>=List<b>。
为了解决上文产生的问题,.NET 4.0 引入了协变。
我们来考虑做一个更加易懂的例子。在我家里,我有一些家电和一些家具。我想要用c#将它们表示出来:
public abstract class HomeAppliance { public int Price { get; set; } } public abstract class ElectronicProduct : HomeAppliance { } public abstract class Furniture : HomeAppliance { }
我家家电有LcdTV和笔记本电脑,家具有床和桌子,这些类型看起来像这样:
public class Bed : Furniture { public Bed() { Price = 320; } } public class Table : Furniture { public Table() { Price = 120; } } public class LcdTV : ElectronicProduct { public LcdTV() { Price = 100; } } public class Laptop : ElectronicProduct { public Laptop() { Price = 200; } }
上边的代码很直接,就是使用了简单的继承。现在我想计算我资产的总价,所以要实现一个类似下边的就算方法:
class Utility { public int CalCulatePrice(List<HomeAppliance> lstHomeAppliance) { var total = 0; lstHomeAppliance.ForEach(p => { total += p.Price; }); return total; } }
测试下,先来算算我家家电的价格。
var listOfElectronicProducts = new List<ElectronicProduct>() { new LcdTV(), new Laptop() }; int totalPriceOfElectronicProducts = new Utility().CalCulatePrice(listOfElectronicProducts);
在编译时,却产生了编译错误。
想想上边的代码,哪里产生的这个问题?在我的CalculatePrice方法中,如果我在循环语句之前添加这么一行:
lstHomeAppliance.Add(new Bed());
单独来看,这个语句添加到CalculatePrice方法中,没有任何问题。而实际上,这样的代码意味着,如果不产生异常,我可以把家具添加到家电列表里。看过上边类图的你可以注意到,家电和家具是两个不同的分支:
List<ElectronicProduct>() { new LcdTV(), new Laptop() };
所以listOfElectronicProducts 不应该允许任何家具添加进来。我想现在你应该能理解,刚才这个异常产生的原因了。好的,我们继续将讨论进行,需要在CalculatePrice方法中把每一类型分别当做参数吗?
public int CalCulatePrice(List<ElectronicProduct> electronicProduct,List<Furniture> furniture) { .... }
.NET 4.0是不需要的,我们有协变来解决这种情况。IEnumerable<out T> 接口在传递参数时使用了out这个关键字,这个关键字告诉编译器你会使用T类型或者它的任何子类。
IEnumerable<out T> : IEnumerable
所以,我只要改变CalCulatePrice函数的参数就可以了。为什么编译错误不再发生?因为这里使用了out关键字,我们告诉编译器,在这个继承自IEnumerable的参数里子类可以替代基类的位置。
CalCulatePrice(IEnumerable<HomeAppliance> lstHomeAppliance)
在编译器的作用下,支持协变的参数可以看起来像实现多态性一样。假设你拥有一个基类和它的一个子类,分别命名为Base和Derived,多态性允许你声明一个Base类变量而用一个Derived类的实例化方法来实例化它。类似的,因为IEnumerable<T>接口支持协变,所以可以使用如下代码来表示:
IEnumerable<Derived> d = new List<Derived>(); IEnumerable<Base> b = d;
转换到我们的场景中:
IEnumerable<ElectronicProduct> listOfElectronicProducts = new List<ElectronicProduct>(); IEnumerable<HomeAppliance> lstHomeAppliance= listOfElectronicProducts ; List<ElectronicProduct> List<Furniture>都是IEnumerable<HomeAppliance>的子类,所以下面的代码将运行正确。 var listOfElectronicProducts = new List<ElectronicProduct>() { new LcdTV(), new Laptop() }; int totalPriceOfElectronicProducts = new Utility().CalCulatePrice(listOfElectronicProducts); //输出: 300 var listOfFurnitures = new List<Furniture>() { new Bed(), new Table() }; int totalPricecOfFurnitures = new Utility().CalCulatePrice(listOfFurnitures); //输出:440 var listOfHomeAppliance = new List<HomeAppliance>() { new Bed(), new Table(), new LcdTV(), new Laptop() }; int totalOfHomeAppliance = new Utility().CalCulatePrice(listOfHomeAppliance); //输出:740
下面,我们把协变的使用推而广之。现在我想输出每个独立产品的信息,为了更加符合要求,我对刚才编写的HomeAppliance做了调整,去掉了其中的Price
这个属性,并且开发了IProductItem<T>接口,用于存放产品的信息。
interface IProductItem<T> { string Name { get; set; } int Price { get; set; } } //实现产品类 class HouseProductItem<T> : IProductItem<T> { public string Name { get; set; } public int Price { get; set; } public HouseProductItem(int price, string name) { this.Name = name; this.Price = price; } }
然后添加一个输出独立产品信息的方法。
public string GetProductInfo(IProductItem<HomeAppliance> HomeAppliance) { return "Product name is " + HomeAppliance.Name + " price is " + HomeAppliance.Price; }
到目前为止,基本的方法都实现了,需要输出信息时,我们使用如下调用就可以了:
var laptop = new HouseProductItem<Laptop>(100, "Laptop"); var info = new Utility().GetProductInfo(laptop);
你也许看出来了,上述代码会产生我们前面遇到的编译错误。因为我们创建的接口,并没有指定支持协变。所以只要做一个简单的修改就可以了:
interface IProductItem<out T> { string Name { get; set; } int Price { get; set; } }
既然List<T>
或者 IEnumerable<T>支持了协变,那么数组呢?我们通过代码来看一下:
public int CalCulatePrice(HomeAppliance[] homeApplicance) { var total = 0; homeApplicance[0] = new Laptop(); homeApplicance.ToList().ForEach(p => { total += p.Price; }); return total; } var arrayOfFurniture = new Furniture[] { new Bed(), new Table() }; int totalPricecOfFurnitures = new Utility().CalCulatePrice(arrayOfFurniture);
编译可以通过,但是很遗憾会抛出运行时异常。