了解C#中的协变和逆变接口


86

我在使用C#阅读的教科书中遇到了这些问题,但是由于缺乏上下文,我很难理解它们。

关于它们是什么以及对它们有用的内容,是否有一个很好的简洁解释?

编辑以澄清:

协变介面:

interface IBibble<out T>
.
.

换向接口:

interface IBibble<in T>
.
.

3
这是一个短期和良好的expalnation恕我直言:blogs.msdn.com/csharpfaq/archive/2010/02/16/...
digEmAll

可能有用:博客文章
Krunal

嗯,这很好,但并不能解释为什么这真让我感到困惑。
NibblyPig 2010年

Answers:


144

使用<out T>,您可以将接口引用视为层次结构中的向上引用。

使用<in T>,您可以将接口引用在层次结构中视为向下。

让我尝试用更多的英语术语进行解释。

假设您正在从动物园中检索动物清单,并且打算对其进行处理。所有动物(在您的动物园中)都有一个名称和唯一的ID。有些动物是哺乳动物,有些是爬行动物,有些是两栖动物,有些是鱼,等等,但它们都是动物。

因此,在您的动物列表(其中包含不同类型的动物)中,您可以说所有的动物都有一个名字,因此显然可以安全地获得所有动物的名字。

但是,如果仅列出鱼类,但需要像对待动物一样对待鱼类,那行得通吗?直观上讲,它应该可以工作,但是在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>

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

使用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 2010年

1
请看最后一点,这是编译器之前禁止的内容,输入和输出的目的是说您可以使用安全的接口(或类型)做什么,以便编译器不会阻止您执行安全的操作。
Lasse V. Karlsen

很棒的答案,我看了他们的视频非常有帮助,并结合您的示例对我进行了排序。仅剩下一个问题,这就是为什么要求“出库”和“入库”,为什么Visual Studio不会自动知道您要做什么(或者其背后的原因是什么)?
NibblyPig 2010年

当谈到类之类的东西时,Automagic的“我明白你要在这里做什么”通常不被接受,最好让程序员明确地标记类型。您可以尝试将“ in”添加到具有返回T的方法的类中,编译器会抱怨。想象一下,如果它悄无声息地删除了以前自动为您添加的“内容”,将会发生什么情况。
Lasse V. Karlsen

1
如果一个关键字需要这种冗长的解释,则显然是错误的。我认为,在这种特殊情况下,C#会变得过于聪明。但是,谢谢您的精彩解释。
rr-

7

是我读过的关于该主题的最佳文章

简而言之,协方差/协方差/不变性涉及自动类型转换(从基础到派生,反之亦然)。只有在对强制转换对象执行的读/写操作方面遵守某些保证的前提下,这些类型强制转换才是可能的。阅读该帖子以获取更多详细信息。


5
链接似乎无效。这是一个存档版本:web.archive.org/web/20140626123445/http
//adamnathan.co.uk/…– si618

1
我更喜欢这个解释:codepureandsimple.com/…–
volkit
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.