逆变量的常见编程用法是什么?

时间:2013-10-20 08:55:31

标签: c# java c++ covariance contravariance

我已阅读以下有关逆差异的文章和Lasse V. Karlsen的回答:

Understanding Covariant and Contravariant interfaces in C#

即使我理解这个概念,我也不明白为什么它有用。 例如,为什么有人会制作只读列表(如帖子中所示:List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>

我也知道重写方法的参数可能是反差的(从概念上讲。据我所知,这在C#,Java和C ++中没有使用)。 哪些例子有意义?

我很欣赏一些简单的现实世界的例子。

3 个答案:

答案 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); FruitElectronics,依此类推。但因为它们都来自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>的实例,因为如果您可以将车辆添加到存储库,则可以将汽车写入存储库。

这解释了outin关键字的协方差和逆变背后的原因。

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

答案 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现在只是 参数多态 (现在也没有)应用于委托和接口。 :)