在C#中理解协变和逆变接口

94
我在一本关于C#的教材中遇到了这些术语,但由于缺乏上下文,我很难理解它们。有没有一个简明扼要的解释说明它们是什么以及它们有什么作用?
澄清编辑:
协变接口:
interface IBibble<out T>
.
.

逆变接口:

interface IBibble<in T>
.
.

3
这是我个人认为的一个简短而好的解释: http://blogs.msdn.com/csharpfaq/archive/2010/02/16/covariance-and-contravariance-faq.aspx - digEmAll
1
可能有用:[博客文章](http://weblogs.asp.net/paulomorgado/archive/2010/04/13/c-4-0-covariance-and-contravariance-in-generics.aspx) - Krunal
嗯,这很好,但它没有解释“为什么”,这正是让我感到困惑的地方。 - NibblyPig
2个回答

157
通过使用<out T>,您可以将接口引用视为层次结构中的上一级。
通过使用<in T>,您可以将接口引用视为层次结构中的下一级。
让我试着用更通俗易懂的语言解释一下。
假设您正在从动物园检索动物列表,并打算对它们进行处理。所有动物(在您的动物园)都有一个名称和一个唯一的ID。有些动物是哺乳动物,有些是爬行动物,有些是两栖动物,有些是鱼类等等,但它们都是动物。
因此,使用包含不同类型动物的动物列表,您可以说所有动物都有一个名称,因此显然安全的是获取所有动物的名称。
但是,如果您只有一个鱼的列表,但需要像对待动物一样对待它们,这是否有效呢?直觉上应该有效,但是在C#3.0及之前版本中,此代码将无法编译:
IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>

这是因为编译器不知道你的意图,或者说,在检索到动物集合后,无法确定可以执行什么操作。在它看来,可能有一种通过 IEnumerable 将对象放回列表的方法,这可能会导致将非鱼类动物放入只应包含鱼类的集合中。
换句话说,编译器无法保证不允许这样做:
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>,只是因为所有的鱼都是动物,所以如果我们将类型更改为:

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中的inout关键字专门将接口标记为一种或另一种。使用in,您可以将通常为T的泛型类型放置在输入位置,这意味着方法参数和只写属性。

使用out,您可以将泛型类型放置在输出位置,即方法返回值、只读属性和out方法参数。

这将使您能够按照代码预期执行所需操作:

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> 在 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

这里有一个例子:

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

嗯,你能解释一下协变和逆变的目标吗?这可能会帮助我更好地理解它。 - NibblyPig
1
看最后一点,编译器在此之前已经防止了这种情况,in和out的目的是告诉你可以对接口(或类型)进行哪些安全操作,以便编译器不会阻止你做安全的事情。 - Lasse V. Karlsen
一个没有提到的问题是嵌套泛型会发生什么。在与T协变的接口中,函数可能返回其他与T协变的接口,或者接受与T逆变的接口。在与T逆变的接口中,函数可能返回与T逆变的接口,或者接受与T协变的接口。例如,虽然昆虫消费者可以替代蚂蚁消费者但不能替代动物消费者,但昆虫消费者的消费者将替代动物消费者的消费者,但不替代蚂蚁消费者的消费者。 - supercat
2
如果一个关键字需要这么长的解释,显然有些不对劲。在我看来,在这种特定情况下,C#试图过于聪明了。尽管如此,还是感谢您提供了清晰的解释。 - rr-
难道这个例子不应该是这样的吗?List fishes = GetAccessToFishes(); // 由于某种原因,返回了 List Fish firstFist = fishes[0]; - DenninDalke
显示剩余3条评论

10

这篇文章 是我读过的关于这个主题的最好文章。

简单来说,协变性 / 逆变性 / 不变性处理自动类型转换(从基类到派生类或者反之)。只有在对转换后的对象执行读取/写入操作时满足一些保证,才能进行这些类型转换。 阅读该文章以获取更多详细信息。


5
链接已失效。这是一个已存档的版本:https://web.archive.org/web/20140626123445/http://adamnathan.co.uk/?p=75 - si618
1
我更喜欢这个解释:https://codepureandsimple.com/covariance-and-contravariance-with-c-410fc4102a02 - volkit

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接