了解C#中的协变和逆变接口
我在C#上阅读的教科书中遇到过这些问题,但是由于缺乏上下文,我很难理解它们。
有一个很好的简洁的解释,他们是什么,他们在那里有什么用?
编辑澄清:
协变界面:
interface IBibble<out T> . .
反变化界面:
interface IBibble<in T> . .
使用<out T>
,您可以将接口引用视为一个向上的层次结构。
使用<in T>
,可以将界面引用作为一个向下在hiearchy中。
让我试着用更多的英文来解释它。
假设您正在从您的动物园中检索动物列表,并且您打算处理它们。 所有的动物(在你的动物园)都有一个名字和一个唯一的ID。 一些动物是哺乳动物,一些是爬行动物,一些是两栖动物,一些是鱼等,但都是动物。
所以,用你的动物列表(包含不同types的动物),你可以说所有的动物都有一个名字,所以显然所有动物的名字是安全的。
但是,如果你只有一个鸟类的名单,但需要像动物一样对待他们,这是否有效? 直观地说,它应该工作,但在C#3.0和以前,这段代码将不会编译:
IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>
原因是编译器在获取它之后并不“知道”你打算或者可以做什么。 众所周知,通过IEnumerable<T>
可以通过一种方法将一个对象放回到列表中,这样可能会允许您将一个不是鱼的动物放入一个只包含鱼。
换句话说,编译器不能保证这是不允许的:
animals.Add(new Mammal("Zebra"));
所以编译器直接拒绝编译你的代码。 这是协变性。
我们来看看逆变。
由于我们的动物园可以处理所有的动物,它当然可以处理鱼,所以我们试着给我们的动物园添加一些鱼。
在C#3.0和之前,这不会编译:
List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal> fishes.Add(new Fish("Guppy"));
在这里,编译器可以允许这段代码,即使这个方法返回List<Animal>
只是因为所有的鱼都是动物,所以如果我们只是改变了types:
List<Animal> fishes = GetAccessToFishes(); fishes.Add(new Fish("Guppy"));
然后,它会工作,但编译器不能确定你不是要这样做:
List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal> Fish firstFist = fishes[0];
由于列表实际上是一个动物列表,这是不允许的。
所以对立和协变是你如何看待对象引用,以及你可以用它们做什么。
C#4.0中的in
和out
关键字专门将接口标记为一个或另一个。 在in
,允许将genericstypes(通常是T)放入input位置,这意味着方法参数和只写属性。
不用out
,你可以将通用types放在输出位置,这是方法返回值,只读属性和输出方法参数。
这将允许你做什么打算与代码:
IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish> // since we can only get animals *out* of the collection, every fish is an animal // so this is safe
List<T>
在List<T>
有向内和向外的方向,所以既不是共变也不是反变,而是一个允许你添加对象的接口,如下所示:
interface IWriteOnlyList<in T> { void Add(T value); }
会允许你这样做:
IWriteOnlyList<Fish> fishes = GetWriteAccessToAnimals(); // still returns IWriteOnlyList<Animal> fishes.Add(new Fish("Guppy")); <-- this is now safe
以下是一些显示这些概念的video:
- 协变和逆变 – VS2010 C#3的第1部分
- 协变和逆变 – VS2010 C#中的第2部分
- 协变和逆变 – VS2010 C#第3部分
这是一个例子:
namespace SO2719954 { class Base { } class Descendant : Base { } interface IBibbleOut<out T> { } interface IBibbleIn<in T> { } class Program { static void Main(string[] args) { // We can do this since every Descendant is also a Base // and there is no chance we can put Base objects into // the returned object, since T is "out" // We can not, however, put Base objects into b, since all // Base objects might not be Descendant. IBibbleOut<Base> b = GetOutDescendant(); // We can do this since every Descendant is also a Base // and we can now put Descendant objects into Base // We can not, however, retrieve Descendant objects out // of d, since all Base objects might not be Descendant IBibbleIn<Descendant> d = GetInBase(); } static IBibbleOut<Descendant> GetOutDescendant() { return null; } static IBibbleIn<Base> GetInBase() { return null; } } }
没有这些标记,可以编译以下内容:
public List<Descendant> GetDescendants() ... List<Base> bases = GetDescendants(); bases.Add(new Base()); <-- uh-oh, we try to add a Base to a Descendant
或这个:
public List<Base> GetBases() ... List<Descendant> descendants = GetBases(); <-- uh-oh, we try to treat all Bases as Descendants
这篇文章是我读过的最好的文章
简而言之,协变/反变换/不变性涉及自动types转换(从基础到派生,反之亦然)。 这些types转换只有在对铸造对象执行的读/写操作方面遵守一些保证时才是可能的。 阅读post了解更多详情。