2010-04-18 61 views
105

我在理解我将如何在现实世界中使用协变和逆变方面有点麻烦。协变和逆变现实世界的例子

到目前为止,我见过的唯一例子就是旧的数组例子。

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

很高兴看到一个例子,如果我能看到它在其他地方被使用,我可以在开发过程中使用它。

+0

我探讨*本【答案】协方差*(http://stackoverflow.com/a/17231728/11545)到(我自己)的问题:协方差类型:举例](HTTP:// stackoverflow.com/q/17231577/11545)。我想你会发现它很有趣,并且很有启发性。 – 2013-06-28 11:15:10

回答

81

假设你有一个班级Person和一个从班级派生出来的班级,老师。你有一些以IEnumerable<Person>作为参数的操作。在你的学校课程中,你有一个返回IEnumerable<Teacher>的方法。协方差允许您直接将结果用于采用IEnumerable<Person>的方法,用较少派生(更通用)的类型替代更多派生类型。直观地反向变换允许您使用更通用的类型,其中指定了更多的派生类型。另请参见https://msdn.microsoft.com/en-us/library/dd799517.aspx

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<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()); 
+23

老师作为一个人(人类)?这不是一个真实世界的例子... – 2014-12-04 23:34:47

+11

@FilipBartuzi - 如果像我一样,当我写这个答案时,你受雇于一个非常真实的世界示例的大学。 – tvanfosson 2014-12-05 02:16:09

+1

当它不回答这个问题并且不给出在c#中使用co/contra方差的任何示例时,如何标记答案? – barakcaf 2016-06-28 12:02:44

30
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>' 
} 

基本上只要你有一个函数,它有一个类型的枚举的,你不能在一个可枚举派生类型的传递没有明确铸造它。

只是要提醒你一个陷阱,但:

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中的行为改变可能引入微妙和艰难,如果你使用一个结构发现错误喜欢这个。

+0

这有助于我更多地理解,谢谢Michael – 2010-04-18 13:38:56

+0

因此,这会影响集合,而不是任何事情,因为在c#3中,可以将更多派生类型传递到派生类型较少的方法。 – 2010-04-18 13:50:28

+2

是的,最大的变化是IEnumerable现在支持这一点,而之前没有。 – 2010-04-18 13:51:58

102
// 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?? 
+98

我喜欢这个现实的例子。上周我刚刚写了一些驴吞噬的代码,我很高兴现在有了协变。 :-) – 2010-04-18 15:26:53

+0

@EricLippert donkeygobberler是逆变不协变。尽管如此,它仍然很酷。 – javadba 2014-02-02 05:27:33

+4

以上评论与@javadba告诉EricLippert什么是协变和逆变是一个现实的协变的例子告诉我的奶奶如何吸鸡蛋! :p – 2014-12-30 13:47:40

45

的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 
+2

这是迄今为止最清楚的答案,谢谢! – Hannish 2017-01-12 12:05:01

+0

假设鱼是动物的一种亚型。很好的答案。 – 2017-04-10 14:07:10

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; 
} 
59

这是我放在一起,帮助我理解上的差异

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 
+4

这是迄今为止我看到的最清晰,最简洁的例子! – 2016-08-21 17:14:52

5

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

协方差

协方差被广泛用不可变的集合使用(即其中,新元素不能被添加或从集合中删除)

给出的简单的类层次结构:

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

看似的方法,例如,因为这

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

...应该接受一个异类收藏:(它它)

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

但是,通过集合更衍生类型失败!

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

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

为什么?由于通用参数IList<LifeForm>不是协变 - IList<LifeForm>是不变的,只接受集合(实现IList),其中参数化类型T必须为LifeForm

如果我恶意更改的方法实现(但保持同样的方法签名),之所以编译器可以防止通过List<Giraffe>变得很明显:

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

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

解决方案是确保使用协变通用类型,例如, IEnumerable(定义为IEnumerable<out T>)。这防止改变到集合,并且作为结果,具有LifeForm亚型任何集合现在可以传递给该方法:

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

DoSomethingWithLifeForms()现在可以与ZebrasGiraffesLifeForm任何亚类的任何IEnumerable<>被称为

逆变

当功能被作为参数传递逆变被频繁使用。

这里是一个函数,它接受一个Action<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<object>作为参数需要Action<Zebra>?传递),如果你解压的步骤,你会注意到,被调用的函数(PerformZebraAction)本身是负责传递数据(在本例中为Zebra实例)添加到该函数 - 数据不来自调用代码。

由于以这种方式使用高阶函数的反转方法,调用Action时,它是更多派生的对象实例,它是针对zebraAction函数调用的(作为参数传递的),它本身使用派生类型越少。

1

转换器的委托帮我想象这两个概念一起工作:

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();