我已阅读以下有关逆差异的文章和Lasse V. Karlsen的回答:
Understanding Covariant and Contravariant interfaces in C#
即使我理解这个概念,我也不明白为什么它有用。
例如,为什么有人会制作只读列表(如帖子中所示:List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
)
我也知道重写方法的参数可能是反差的(从概念上讲。据我所知,这在C#,Java和C ++中没有使用)。 哪些例子有意义?
我很欣赏一些简单的现实世界的例子。
答案 0 :(得分:3)
(我认为这个问题更多的是关于协方差而不是逆变,因为引用的例子与协方差有关。)
List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
脱离背景,这有点误导。在您引用的示例中,作者打算传达这样的想法:技术上,如果List<Fish>
实际上引用List<Animal>
将安全地添加{{1}它。
但当然,这也可以让你添加一个Fish
- 这显然是错误的。
因此编译器不允许您为Cow
引用分配List<Animal>
引用。
那么这什么时候才真正安全 - 而且有用呢?
如果无法修改集合,则为安全分配。在C#中,List<Fish>
可以表示不可修改的集合。
所以你可以安全地做到这一点:
IEnumerable<T>
因为没有可能将非Fish添加到IEnumerable<Animal> animals = GetAccessToFishes(); // for some reason, returns List<Animal>
。它没有办法允许你这样做。
那么这什么时候有用?
每当您想要访问某个集合的常见方法或属性时,这非常有用,该集合可以包含从基类派生的一个或多个类型的项目。
例如,您可能有一个层次结构,表示超市的不同类别的库存。
假设基类animals
具有属性StockItem
。
我们还假设您有一个方法double SalePrice
,它返回一个Shopper.Basket()
,表示购物者在购物篮中的商品。篮子中的项目可以是源自IEnumerable<StockItem>
的任何具体类型。
在这种情况下,您可以添加购物篮中所有商品的价格(我已经写了这个长手而不使用Linq来弄清楚发生了什么。真正的代码当然会使用StockItem
):
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
,所以代码适用于所有这些代码。
希望这种代码的实用性很明显!这与Linq中许多方法的工作方式非常相似。
答案 1 :(得分:1)
协方差对只读(out)存储库很有用;只写(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
关键字的协方差和逆变背后的原因。
至于为什么你可能想要进行这种分离(使用单独的只读和只写接口,这很可能是由同一个具体类实现的),这有助于你保持命令之间的清晰分离(写操作)和查询(读取操作),它们可能对性能,一致性和可用性有不同的要求,因此需要在代码库中采用不同的策略。
答案 2 :(得分:0)
如果你没有真正掌握逆变的概念,那么这样的问题会发生(并且会发生)。
让我们构建一个最简单的例子:
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&lt;&gt;的方法。通过这种方式,您将始终为您的对象调用正确的方法。 Covariance和ContraVariance现在只是 参数多态 (现在也没有)应用于委托和接口。 :)