7

我已经阅读了以下关于反方差和 Lasse V. Karlsen 的回答的帖子:

了解 C# 中的协变和逆变接口

尽管我理解这个概念,但我不明白它为什么有用。例如,为什么有人会制作一个只读列表(就像在帖子中一样List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>:)

我也知道覆盖方法的参数可以是逆变的(从概念上讲。据我所知,这在 C#、Java 和 C++ 中没有使用)。有哪些例子可以说明这一点?

我会很感激一些简单的现实世界的例子。

4

3 回答 3

3

(我认为这个问题更多的是关于协方差而不是逆变,因为引用的示例与协方差有关。)

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>

断章取义,这有点误导。在您引用的示例中,作者打算传达这样一种想法,即从技术上讲,如果List<Fish>实际引用 a List<Animal>则将a 添加到它是安全的Fish

但是,当然,这也可以让您添加 a Cow——这显然是错误的。

因此,编译器不允许您将引用分配给List<Animal>引用List<Fish>

那么什么时候这实际上是安全和有用的呢?

如果集合不能被修改,这是一个安全的分配。在 C# 中,anIEnumerable<T>可以表示不可修改的集合。

所以你可以安全地做到这一点:

IEnumerable<Animal> animals = GetAccessToFishes(); // for some reason, returns List<Animal>

因为不可能将非鱼添加到animals. 它没有允许您这样做的方法。

那么这什么时候有用呢?

每当您想要访问可以包含从基类派生的一种或多种类型的项目的集合的某些公共方法或属性时,这很有用。

例如,您可能有一个层次结构表示超市的不同库存类别。

假设基类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);

在此示例中,篮子中的项目类型可能是FruitElectronicsClothing。但是因为它们都派生自StockItem,所以代码适用于所有这些。

希望这种代码的实用性很明确!这与 Linq 中许多方法的工作方式非常相似。

于 2013-10-20T09:17:19.370 回答
1

协方差对于只读(出)存储库很有用;只写(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

至于为什么您可能想要进行这种分离(使用单独的只读和只写接口,这很可能由同一个具体类实现),这有助于您在命令(写操作)和查询之间保持清晰的分离(读取操作),这可能对性能、一致性和可用性有不同的要求,因此在您的代码库中需要不同的策略。

于 2013-10-20T09:44:14.100 回答
1

如果您没有真正掌握逆变的概念,那么可能(并且将会)出现这样的问题。

让我们构建一个最简单的例子:

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<> 的方法。这样,您将始终为您的对象调用正确的方法。Covariance 和 ContraVariance 只是现在应用于委托和接口的参数多态性(仅此而已)。:)

于 2013-10-20T10:56:49.543 回答