[译]c#中的协变-Covariance in C#

本文发表于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);

编译可以通过,但是很遗憾会抛出运行时异常。

[译]c#中的协变-Covariance in C#_第1张图片 

你可能感兴趣的:(var)