理解C#中的Covariant和Contravariant接口

Nib*_*Pig 82 .net c# interface covariance contravariance

我在C#上阅读的教科书中遇到过这些,但我很难理解它们,可能是由于缺乏背景.

是否有一个很简洁的解释,说明它们是什么以及它们对那里有用的东西?

编辑以澄清:

协变界面:

interface IBibble<out T>
.
.
Run Code Online (Sandbox Code Playgroud)

逆变接口:

interface IBibble<in T>
.
.
Run Code Online (Sandbox Code Playgroud)

ang*_*son 138

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

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

让我试着用更多的英语术语来解释它.

假设您正在从动物园中检索动物列表,并且您打算处理它们.所有动物(在你的动物园里)都有一个名字和一个唯一的ID.有些动物是哺乳动物,有些是爬行动物,有些是两栖动物,有些是鱼类等,但它们都是动物.

因此,根据您的动物清单(其中包含不同类型的动物),您可以说所有动物都有一个名字,所以显然获得所有动物的名称是安全的.

但是,如果你只有一份鱼类清单,但需要像动物一样对待它们,那会起作用吗?直观地说,它应该可以工作,但是在C#3.0之前,这段代码将无法编译:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>
Run Code Online (Sandbox Code Playgroud)

这样做的原因是编译器在您检索动物集合后不会"知道"您想要或可以对动物集合执行的操作.据他所知,可以通过一种方法IEnumerable<T>将一个物体放回到列表中,这可能会让你把一只不是鱼的动物放入一个本应只包含鱼的集合中.

换句话说,编译器不能保证不允许这样做:

animals.Add(new Mammal("Zebra"));
Run Code Online (Sandbox Code Playgroud)

所以编译器完全拒绝编译你的代码.这是协方差.

让我们来看看逆差.

由于我们的动物园可以处理所有动物,它当然可以处理鱼类,所以让我们尝试在我们的动物园里添加一些鱼.

在C#3.0和之前,这不编译:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
fishes.Add(new Fish("Guppy"));
Run Code Online (Sandbox Code Playgroud)

在这里,编译器可以允许这段代码,即使该方法返回List<Animal>只是因为所有的鱼都是动物,所以如果我们只是将类型更改为:

List<Animal> fishes = GetAccessToFishes();
fishes.Add(new Fish("Guppy"));
Run Code Online (Sandbox Code Playgroud)

然后它会工作,但编译器无法确定您没有尝试这样做:

List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
Fish firstFist = fishes[0];
Run Code Online (Sandbox Code Playgroud)

由于该列表实际上是动物列表,因此不允许这样做.

所以反对和共同变化是你如何对待对象引用以及你可以用它们做什么.

C#4.0中的inout关键字专门将接口标记为一个或另一个.使用in,您可以将泛型类型(通常为T)放在input -positions中,这意味着方法参数和只写属性.

使用时out,您可以将泛型类型放在output -positions中,这是方法返回值,只读属性和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
Run Code Online (Sandbox Code Playgroud)

List<T> 在T上有向内和向外的方向,所以它既不是共同变体也不是反变体,而是一个允许你添加对象的接口,如下所示:

interface IWriteOnlyList<in T>
{
    void Add(T value);
}
Run Code Online (Sandbox Code Playgroud)

会允许你这样做:

IWriteOnlyList<Fish> fishes = GetWriteAccessToAnimals(); // still returns
                                                            IWriteOnlyList<Animal>
fishes.Add(new Fish("Guppy")); <-- this is now safe
Run Code Online (Sandbox Code Playgroud)

以下是一些显示概念的视频:

这是一个例子:

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;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

没有这些标记,以下内容可以编译:

public List<Descendant> GetDescendants() ...
List<Base> bases = GetDescendants();
bases.Add(new Base()); <-- uh-oh, we try to add a Base to a Descendant
Run Code Online (Sandbox Code Playgroud)

或这个:

public List<Base> GetBases() ...
List<Descendant> descendants = GetBases(); <-- uh-oh, we try to treat all Bases
                                               as Descendants
Run Code Online (Sandbox Code Playgroud)

  • 如果一个关键字需要这么长的解释,那么显然出了问题。在我看来,C# 在这种特殊情况下试图变得过于聪明。尽管如此,还是谢谢你的精彩解释。 (2认同)

Ben*_*n G 6

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

简而言之,协方差/逆变/不变性处理自动类型转换(从基础到派生,反之亦然).只有在对铸造对象执行的读/写操作方面遵守某些保证时,才可能进行这些类型转换.阅读帖子了解更多详情.

  • 链接似乎死了.这是一个存档版本:https://web.archive.org/web/20140626123445/http://adamnathan.co.uk/?p = 75 (5认同)