我已经阅读了以下关于反方差和Lasse V.Karlsen的答案的帖子:
理解C#中的协变和逆变接口
尽管我理解这个概念,但我不明白为什么它有用。例如,为什么有人会制作只读列表(如帖子中的List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
(
我也知道重写方法的参数可以是反方差的(从概念上讲。据我所知,这在C#、Java和C++中没有使用(。有哪些例子可以说明这一点?
我很欣赏一些简单的现实世界的例子。
(我认为这个问题更多的是关于协方差而不是方差,因为引用的例子是关于协方差的。(
List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
断章取义,这有点误导。在你引用的例子中,作者打算传达这样一个想法,即从技术上讲,如果List<Fish>
实际上引用了List<Animal>
,那么在其中添加Fish
是安全的。
但当然,这也会让你在其中添加一个Cow
——这显然是错误的。
因此,编译器不允许将List<Animal>
引用分配给List<Fish>
引用。
那么,什么时候这才是真正安全和有用的呢?
如果不能修改集合,则这是一个安全的分配。在C#中,IEnumerable<T>
可以表示不可修改的集合。
所以你可以安全地做到这一点:
IEnumerable<Animal> animals = GetAccessToFishes(); // for some reason, returns List<Animal>
因为不可能向CCD_ 11添加非Fish。它没有允许你这样做的方法。
那么这什么时候有用呢
当您想要访问某个集合的某些公共方法或属性时,这一点非常有用,该集合可以包含从基类派生的一个或多个类型的项。
例如,您可能有一个层次结构,代表超市的不同库存类别。
假设基类StockItem
具有属性double SalePrice
。
假设您有一个方法Shopper.Basket()
,它返回一个IEnumerable<StockItem>
,代表购物者购物篮中的商品。篮子中的物品可以是源自StockItem
的任何具体种类。
在这种情况下,你可以添加购物篮中所有商品的价格(我在没有使用Linq的情况下写了这篇长文来明确发生了什么。当然,真正的代码会使用IEnumerable.Sum()
(:
IEnumerable<StockItem> itemsInBasket = shopper.Basket;
double totalCost = 0.0;
foreach (var item in itemsInBasket)
totalCost += item.SalePrice;
对比
例如,当您希望通过基类类型将某些操作应用于项或项集合时,即使您有派生类型。
例如,您可以有一个方法,将一个操作应用于StockItem
序列中的每个项目,如下所示:
void ApplyToStockItems(IEnumerable<StockItem> items, Action<StockItem> action)
{
foreach (var item in items)
action(item);
}
使用StockItem
示例,假设它有一个Print()
方法,您可以使用该方法将其打印到收银台收据上。你可以打电话然后这样使用:
Action<StockItem> printItem = item => { item.Print(); }
ApplyToStockItems(shopper.Basket, printItem);
在这个例子中,购物篮中的项目类型可能是Fruit
、Electronics
、Clothing
等等。但由于它们都源自StockItem
,所以代码可以处理所有这些项目。
希望这类代码的实用性是清楚的!这与Linq中的许多方法的工作方式非常相似。
协方差对于只读(输出(存储库很有用;只写(in(存储库的差异。
public interface IReadRepository<out TVehicle>
{
TVehicle GetItem(Guid id);
}
public interface IWriteRepository<in TVehicle>
{
void AddItem(TVehicle vehicle);
}
这样,IReadRepository<Car>
的实例也是IReadRepository<Vehicle>
的实例,因为如果你从存储库中取出一辆车,它也是一辆车;然而,IWriteRepository<Vehicle>
的实例也是IWriteRepository<Car>
的实例,因为如果可以将车辆添加到存储库中,那么就可以将汽车写入存储库。
这解释了out
和in
关键字用于协方差和反方差背后的推理。
至于为什么要进行这种分离(使用单独的只读和只读接口,这很可能由同一个具体类实现(,这有助于在命令(写操作(和查询(读操作(之间保持干净的分离,这可能对性能、一致性和可用性有不同的要求,因此需要在代码库中使用不同的策略。
如果你没有真正理解方差的概念,那么类似的问题可能(也将(发生。
让我们构建一个最简单的例子:
public class Human
{
virtual public void DisplayLanguage() { Console.WriteLine("I do speak a language"); }
}
public class Asian : Human
{
override public void DisplayLanguage() { Console.WriteLine("I speak chinesse"); }
}
public class European : Human
{
override public void DisplayLanguage() { Console.WriteLine("I speak romanian"); }
}
好的,这是一个反方差的例子
public class test
{
static void ContraMethod(Human h)
{
h.DisplayLanguage();
}
static void ContraForInterfaces(IEnumerable<Human> humans)
{
foreach (European euro in humans)
{
euro.DisplayLanguage();
}
}
static void Main()
{
European euro = new European();
test.ContraMethod(euro);
List<European> euroList = new List<European>() { new European(), new European(), new European() };
test.ContraForInterfaces(euroList);
}
}
在.Net 4.0之前,不允许使用ContraForInterfaces方法(所以他们实际上修复了一个BUG:(。
好的,现在ContraForInterfaces方法的含义是什么?很简单,一旦你列出了欧洲人的列表,其中的对象总是欧洲人,即使你把它们传递给一个采用IEnumerable<>的方法。通过这种方式,您将始终为对象调用正确的方法。协方差和反方差只是参数多态性(仅此而已(现在也应用于委托和接口。:(