协方差和反方差的真实世界示例


162

我在理解如何在现实世界中使用协方差和逆方差时遇到了一些麻烦。

到目前为止,我所看到的唯一示例是相同的旧数组示例。

object[] objectArray = new string[] { "string 1", "string 2" };

如果能看到其他地方使用的示例,那么很高兴能在开发期间使用它。


1
我在这个(我自己的)问题的答案中探讨了协方差协方差类型:通过示例。我认为您会发现它很有趣,并希望能对您有所启发。
Cristian Diaconescu

Answers:


109

假设您有一个Person类和一个从其派生的类Teacher。您有一些操作以a IEnumerable<Person>作为参数。在您的School课堂中,您有一个返回的方法IEnumerable<Teacher>。协方差允许您将结果直接用于采用的方法,将IEnumerable<Person>更多派生的类型替换为次派生(更通用)的类型。违反直觉,相反,您可以使用更通用的类型,其中指定了更多的派生类型。

另请参见MSDN上泛型中的协方差和协方差

课程

public class Person 
{
     public string Name { get; set; }
} 

public class Teacher : Person { } 

public class MailingList
{
    public void Add(IEnumerable<out Person> people) { ... }
}

public class School
{
    public IEnumerable<Teacher> GetTeachers() { ... }
}

public class PersonNameComparer : IComparer<Person>
{
    public int Compare(Person a, Person b) 
    { 
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : Compare(a,b);
    }

    private int Compare(string a, string b)
    {
        if (a == null) return b == null ? 0 : -1;
        return b == null ? 1 : a.CompareTo(b);
    }
}

用法

var teachers = school.GetTeachers();
var mailingList = new MailingList();

// Add() is covariant, we can use a more derived type
mailingList.Add(teachers);

// the Set<T> constructor uses a contravariant interface, IComparer<in T>,
// we can use a more generic type than required.
// See https://msdn.microsoft.com/en-us/library/8ehhxeaf.aspx for declaration syntax
var teacherSet = new SortedSet<Teachers>(teachers, new PersonNameComparer());

14
@FilipBartuzi-如果像我写此答案时一样,您受雇于一所大学,那是一个非常真实的例子。
tvanfosson 2014年

5
当它不回答问题并且没有给出在c#中使用co / contra方差的任何示例时,如何将其标记为答案?
barakcaf

@barakcaf添加了一个逆向示例。不知道为什么您看不到协方差示例-也许您需要向下滚动代码-但我对此添加了一些注释。
tvanfosson

@tvanfosson代码使用co / contra,但它没有显示如何声明它。该示例未在通用声明中显示in / out的用法,而另一个答案在其中。
barakcaf

因此,如果我做对了,协方差就是允许Liskov在C#中进行替换的原则,对吗?
米格尔·韦洛索

136
// Contravariance
interface IGobbler<in T> {
    void gobble(T t);
}

// Since a QuadrupedGobbler can gobble any four-footed
// creature, it is OK to treat it as a donkey gobbler.
IGobbler<Donkey> dg = new QuadrupedGobbler();
dg.gobble(MyDonkey());

// Covariance
interface ISpewer<out T> {
    T spew();
}

// A MouseSpewer obviously spews rodents (all mice are
// rodents), so we can treat it as a rodent spewer.
ISpewer<Rodent> rs = new MouseSpewer();
Rodent r = rs.spew();

为了完整性...

// Invariance
interface IHat<T> {
    void hide(T t);
    T pull();
}

// A RabbitHat…
IHat<Rabbit> rHat = RabbitHat();

// …cannot be treated covariantly as a mammal hat…
IHat<Mammal> mHat = rHat;      // Compiler error
// …because…
mHat.hide(new Dolphin());      // Hide a dolphin in a rabbit hat??

// It also cannot be treated contravariantly as a cottontail hat…
IHat<CottonTail> cHat = rHat;  // Compiler error
// …because…
rHat.hide(new MarshRabbit());
cHat.pull();                   // Pull a marsh rabbit out of a cottontail hat??

138
我喜欢这个现实的例子。我上周刚写了一些驴吞噬代码,我很高兴我们现在有了协方差。:-)
埃里克·利珀特

4
上面带有@javadba的注释告诉THE EricLippert什么是协方差和逆方差是我告诉我的奶奶如何吸卵的现实协变示例!:p
iAteABug_And_iLiked_it 2014年

1
这个问题并没有询问协方差和协方差可以做什么,它问你为什么需要使用它。您的示例不切实际,因为它不需要。我可以创建一个QuadrupedGobbler并将其作为自己对待(将其分配给IGobbler <Quadruped>),它仍然可以吞噬驴子(我可以将驴子传递给需要Quadruped的Gobble方法)。无需矛盾。太酷了,我们可以把一个QuadrupedGobbler作为DonkeyGobbler,但我们为什么需要,在这种情况下,如果QuadrupedGobbler已经可以吞噬驴?
wired_in

1
@wired_in因为当您只关心驴子时,会变得更笼统。例如,如果您有一个农场提供要被吞噬的驴子,则可以将其表示为void feed(IGobbler<Donkey> dg)。如果改为使用IGobbler <Quadruped>作为参数,则无法传递仅吃驴的龙。
马塞洛·坎托斯

1
Waaay迟到了聚会,但这是关于SO的最佳书面示例。在荒谬的情况下完全有意义。我将不得不回答我的问题……
Jesse Williams

120

这是我整理来帮助我理解差异的方法

public interface ICovariant<out T> { }
public interface IContravariant<in T> { }

public class Covariant<T> : ICovariant<T> { }
public class Contravariant<T> : IContravariant<T> { }

public class Fruit { }
public class Apple : Fruit { }

public class TheInsAndOuts
{
    public void Covariance()
    {
        ICovariant<Fruit> fruit = new Covariant<Fruit>();
        ICovariant<Apple> apple = new Covariant<Apple>();

        Covariant(fruit);
        Covariant(apple); //apple is being upcasted to fruit, without the out keyword this will not compile
    }

    public void Contravariance()
    {
        IContravariant<Fruit> fruit = new Contravariant<Fruit>();
        IContravariant<Apple> apple = new Contravariant<Apple>();

        Contravariant(fruit); //fruit is being downcasted to apple, without the in keyword this will not compile
        Contravariant(apple);
    }

    public void Covariant(ICovariant<Fruit> fruit) { }

    public void Contravariant(IContravariant<Apple> apple) { }
}

tldr

ICovariant<Fruit> apple = new Covariant<Apple>(); //because it's covariant
IContravariant<Apple> fruit = new Contravariant<Fruit>(); //because it's contravariant

10
这是我迄今为止看到的最好的东西,简洁明了。
罗布L

6
(在Contravariance示例中)当Fruit的父级时,如何将水果转换为苹果Apple
Tobias Marschall

@TobiasMarschall,这意味着您必须进一步研究“多态性”
snr

56

in和out关键字控制具有通用参数的接口和委托的编译器强制转换规则:

interface IInvariant<T> {
    // This interface can not be implicitly cast AT ALL
    // Used for non-readonly collections
    IList<T> GetList { get; }
    // Used when T is used as both argument *and* return type
    T Method(T argument);
}//interface

interface ICovariant<out T> {
    // This interface can be implicitly cast to LESS DERIVED (upcasting)
    // Used for readonly collections
    IEnumerable<T> GetList { get; }
    // Used when T is used as return type
    T Method();
}//interface

interface IContravariant<in T> {
    // This interface can be implicitly cast to MORE DERIVED (downcasting)
    // Usually means T is used as argument
    void Method(T argument);
}//interface

class Casting {

    IInvariant<Animal> invariantAnimal;
    ICovariant<Animal> covariantAnimal;
    IContravariant<Animal> contravariantAnimal;

    IInvariant<Fish> invariantFish;
    ICovariant<Fish> covariantFish;
    IContravariant<Fish> contravariantFish;

    public void Go() {

        // NOT ALLOWED invariants do *not* allow implicit casting:
        invariantAnimal = invariantFish; 
        invariantFish = invariantAnimal; // NOT ALLOWED

        // ALLOWED covariants *allow* implicit upcasting:
        covariantAnimal = covariantFish; 
        // NOT ALLOWED covariants do *not* allow implicit downcasting:
        covariantFish = covariantAnimal; 

        // NOT ALLOWED contravariants do *not* allow implicit upcasting:
        contravariantAnimal = contravariantFish; 
        // ALLOWED contravariants *allow* implicit downcasting
        contravariantFish = contravariantAnimal; 

    }//method

}//class

// .NET Framework Examples:
public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable { }
public interface IEnumerable<out T> : IEnumerable { }


class Delegates {

    // When T is used as both "in" (argument) and "out" (return value)
    delegate T Invariant<T>(T argument);

    // When T is used as "out" (return value) only
    delegate T Covariant<out T>();

    // When T is used as "in" (argument) only
    delegate void Contravariant<in T>(T argument);

    // Confusing
    delegate T CovariantBoth<out T>(T argument);

    // Confusing
    delegate T ContravariantBoth<in T>(T argument);

    // From .NET Framework:
    public delegate void Action<in T>(T obj);
    public delegate TResult Func<in T, out TResult>(T arg);

}//class

假设鱼是动物的一种亚型。好的答案。
Rajan Prasad

48

这是一个使用继承层次结构的简单示例。

给定简单的类层次结构:

在此处输入图片说明

并在代码中:

public abstract class LifeForm  { }
public abstract class Animal : LifeForm { }
public class Giraffe : Animal { }
public class Zebra : Animal { }

不变性(即,通用类型参数*不*inout关键字修饰)

貌似这样的方法

public static void PrintLifeForms(IList<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}

...应该接受一个异构集合:

var myAnimals = new List<LifeForm>
{
    new Giraffe(),
    new Zebra()
};
PrintLifeForms(myAnimals); // Giraffe, Zebra

但是,传递更多派生类型的集合失败!

var myGiraffes = new List<Giraffe>
{
    new Giraffe(), // "Jerry"
    new Giraffe() // "Melman"
};
PrintLifeForms(myGiraffes); // Compile Error!

cannot convert from 'System.Collections.Generic.List<Giraffe>' to 'System.Collections.Generic.IList<LifeForm>'

为什么?由于泛型参数IList<LifeForm>不是协变的- IList<T>是不变的,因此IList<LifeForm>仅接受参数化类型T必须为的集合(实现IList)LifeForm

如果的方法实现PrintLifeForms是恶意的(但具有相同的方法签名),则编译器阻止传递的原因List<Giraffe>显而易见:

 public static void PrintLifeForms(IList<LifeForm> lifeForms)
 {
     lifeForms.Add(new Zebra());
 }

由于IList允许添加或删除元素,LifeForm因此可以将的任何子类添加到参数中lifeForms,并且将违反传递给方法的任何派生类型集合的类型。(在这里,恶意方法将尝试添加Zebravar myGiraffes)。幸运的是,编译器使我们免受了这种危险。

协方差(带有参数化类型的通用,以修饰out

协方差广泛用于不可变集合(即无法在集合中添加或删除新元素的地方)

上面示例的解决方案是确保使用协变通用集合类型,例如IEnumerable(定义为IEnumerable<out T>)。IEnumerable没有更改集合的方法,并且由于out协方差的缘故,LifeForm现在可以将任何具有子类型的集合传递给方法:

public static void PrintLifeForms(IEnumerable<LifeForm> lifeForms)
{
    foreach (var lifeForm in lifeForms)
    {
        Console.WriteLine(lifeForm.GetType().ToString());
    }
}

PrintLifeForms现在可以叫ZebrasGiraffes和任何IEnumerable<>的任何子类的LifeForm

矛盾(带有参数化类型的通用装饰有in

当函数作为参数传递时,经常使用协变性。

这是一个函数示例,该函数将an Action<Zebra>作为参数,并在Zebra的已知实例上调用它:

public void PerformZebraAction(Action<Zebra> zebraAction)
{
    var zebra = new Zebra();
    zebraAction(zebra);
}

如预期的那样,这很好:

var myAction = new Action<Zebra>(z => Console.WriteLine("I'm a zebra"));
PerformZebraAction(myAction); // I'm a zebra

凭直觉,这将失败:

var myAction = new Action<Giraffe>(g => Console.WriteLine("I'm a giraffe"));
PerformZebraAction(myAction); 

cannot convert from 'System.Action<Giraffe>' to 'System.Action<Zebra>'

但是,这成功了

var myAction = new Action<Animal>(a => Console.WriteLine("I'm an animal"));
PerformZebraAction(myAction); // I'm an animal

甚至这也成功了:

var myAction = new Action<object>(a => Console.WriteLine("I'm an amoeba"));
PerformZebraAction(myAction); // I'm an amoeba

为什么?因为Action被定义为Action<in T>,即,其是contravariant,这意味着为Action<Zebra> myAction,其myAction可在“最”一个Action<Zebra>,但较少衍生的超Zebra也可以接受。

尽管起初这可能不直观(例如,如何Action<object>将a作为参数传递给需要Action<Zebra>?),但是如果解压缩步骤,您会注意到被调用的函数(PerformZebraAction)本身负责传递数据(在这种情况下,Zebra实例) )传递给函数-数据不是来自调用代码。

由于以这种方式使用高阶函数的反向方法,在Action调用时,虽然函数本身使用的是较少派生的类型,但Zebra针对zebraAction函数调用的是派生性更高的实例(作为参数传递)。


7
这是对不同方差选项的一个很好的解释,因为它遍历了示例并且还阐明了为什么编译器在没有in / out关键字的情况下限制或允许使用
Vikhram

in关键字在哪里用于反差
javadba

@javadba在上面,Action<in T>并且Func<in T, out TResult>在输入类型上是相反的。(我的示例使用现有的不变量(列表),协变量(IEnumerable)和对变量(动作,
函数

好的,我不这样C#做不会知道这一点。
javadba

它在Scala中非常相似,只是语法不同-[+ T]在T中是协变的,[-T]在T中是协变的,Scala还可以强制执行'between'约束,以及'Nothing'混杂子类,其中C#没有。
StuartLC

32
class A {}
class B : A {}

public void SomeFunction()
{
    var someListOfB = new List<B>();
    someListOfB.Add(new B());
    someListOfB.Add(new B());
    someListOfB.Add(new B());
    SomeFunctionThatTakesA(someListOfB);
}

public void SomeFunctionThatTakesA(IEnumerable<A> input)
{
    // Before C# 4, you couldn't pass in List<B>:
    // cannot convert from
    // 'System.Collections.Generic.List<ConsoleApplication1.B>' to
    // 'System.Collections.Generic.IEnumerable<ConsoleApplication1.A>'
}

基本上,只要您有一个采用一种类型的Enumerable的函数,就不能在未显式强制转换的情况下传递派生类型的Enumerable。

只是为了警告您一个陷阱:

var ListOfB = new List<B>();
if(ListOfB is IEnumerable<A>)
{
    // In C# 4, this branch will
    // execute...
    Console.Write("It is A");
}
else if (ListOfB is IEnumerable<B>)
{
    // ...but in C# 3 and earlier,
    // this one will execute instead.
    Console.Write("It is B");
}

无论如何这都是可怕的代码,但是它确实存在,如果使用这样的结构,C#4中不断变化的行为可能会引入难以发现的错误。


因此,这对集合的影响最大,因为在c#3中,您可以将派生程度更高的类型传递给派生程度较小的方法。
Razor

3
是的,最大的变化是IEnumerable现在支持此功能,而以前则不支持。
Michael Stum

4

MSDN

以下代码示例显示了对方法组的协方差和对数支持

static object GetObject() { return null; }
static void SetObject(object obj) { }

static string GetString() { return ""; }
static void SetString(string str) { }

static void Test()
{
    // Covariance. A delegate specifies a return type as object, 
    // but you can assign a method that returns a string.
    Func<object> del = GetString;

    // Contravariance. A delegate specifies a parameter type as string, 
    // but you can assign a method that takes an object.
    Action<string> del2 = SetObject;
}

4

逆差

在现实世界中,您总是可以使用动物收容所代替兔子收容所,因为每次动物收容所收容兔子都是动物。但是,如果您使用兔子收容所而不是动物收容所,则其工作人员可能会被老虎吞噬。

在代码中,这意味着,如果你有一个IShelter<Animal> animals可以简单的写IShelter<Rabbit> rabbits = animals ,如果你答应和使用TIShelter<T>唯一的方法参数,如下所示:

public class Contravariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface IShelter<in T>
    {
        void Host(T thing);
    }

    public void NoCompileErrors()
    {
        IShelter<Animal> animals = null;
        IShelter<Rabbit> rabbits = null;

        rabbits = animals;
    }
}

并用更通用的商品替换商品,即减少差异或引入对价差异。

协方差

在现实世界中,您总是可以使用兔子的供应商来代替动物的供应商,因为兔子供应商每次给您的兔子都是动物。但是,如果您使用动物供应商而不是兔子供应商,则可能被老虎吃掉。

在代码中,这意味着如果您有一个ISupply<Rabbit> rabbits,则可以简单地编写,ISupply<Animal> animals = rabbits 只要您保证并TISupply<T>方法中将其用作唯一的返回值,例如:

public class Covariance
{
    public class Animal { }
    public class Rabbit : Animal { }

    public interface ISupply<out T>
    {
        T Get();
    }

    public void NoCompileErrors()
    {
        ISupply<Animal> animals = null;
        ISupply<Rabbit> rabbits = null;

        animals = rabbits;
    }
}

并用衍生性更高的商品代替,即增加差异或引入方差。

总而言之,这只是您的编译时可检查的保证,即您将以某种方式对待泛型类型,以确保类型安全并且不会被任何人吃掉。

你可能想给这个一读双绕你的头解决这个问题。


您可以通过一个老虎吃掉 那是值得的给予好评
javadba

您的评论contravariance很有趣。我读它是为了指示操作要求:更通用的类型必须支持从它派生的所有类型的用例。因此,在这种情况下,动物收容所必须能够支持对每种动物类型的收容。在这种情况下,添加新的子类可能会破坏超类!那就是-如果我们添加一个霸王龙的亚型,那么它可能会破坏我们现有的动物收容所
javadba

(继续)。这与在结构上清楚描述的协方差有很大不同:所有更具体的子类型都支持超类型中定义的操作-但不一定以相同的方式。
javadba

3

转换器代表帮助我将两个概念一起可视化:

delegate TOutput Converter<in TInput, out TOutput>(TInput input);

TOutput表示方法返回更特定类型的协方差

TInput表示方法传递不那么具体的类型的协方差

public class Dog { public string Name { get; set; } }
public class Poodle : Dog { public void DoBackflip(){ System.Console.WriteLine("2nd smartest breed - woof!"); } }

public static Poodle ConvertDogToPoodle(Dog dog)
{
    return new Poodle() { Name = dog.Name };
}

List<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } };
List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle));
poodles[0].DoBackflip();
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.