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

Posted

技术标签:

【中文标题】了解 C# 中的协变和逆变接口【英文标题】:Understanding Covariant and Contravariant interfaces in C# 【发布时间】:2011-02-12 18:42:53 【问题描述】:

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

对它们是什么以及它们有什么用处有一个很好的简明解释吗?

编辑澄清:

协变接口:

interface IBibble<out T>
.
.

逆变接口:

interface IBibble<in T>
.
.

【问题讨论】:

这是一个简短而好的解释恕我直言:blogs.msdn.com/csharpfaq/archive/2010/02/16/… 可能有用:Blog Post 嗯,这很好,但它没有解释 为什么 这才是真正让我困惑的地方。 【参考方案1】:

使用&lt;out T&gt;,您可以将接口引用视为层次结构中的上一级。

使用&lt;in T&gt;,您可以将接口引用视为层次结构中的一个向下。

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

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

因此,根据您的动物列表(其中包含不同类型的动物),您可以说所有动物都有一个名称,因此很明显获取所有动物的名称是安全的。

但是,如果您只有一份鱼的清单,但需要像对待动物一样对待它们,这行得通吗?直觉上应该可以,但是在 C# 3.0 及之前的版本中,这段代码将无法编译:

IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>

原因是编译器不“知道”您的意图,或者可以在您检索到动物集合后对其进行处理。据它所知,可能有一种方法通过IEnumerable&lt;T&gt; 将对象放回列表中,这可能允许您将不是鱼的动物放入应该只包含鱼的集合中.

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

animals.Add(new Mammal("Zebra"));

所以编译器直接拒绝编译你的代码。这是协方差。

让我们看看逆变。

既然我们的动物园可以处理所有的动物,它当然可以处理鱼,所以让我们尝试在我们的动物园中添加一些鱼。

在 C# 3.0 及之前的版本中,这不会编译:

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

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

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)放在 input 位置,这意味着方法参数和只写属性。

使用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&lt;T&gt; 在 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

这里有一些展示这些概念的视频:

Covariance and Contravariance - VS2010 C# Part 1 of 3 Covariance and Contravariance - VS2010 C# Part 2 of 3 Covariance and Contravariance - VS2010 C# Part 3 of 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

【讨论】:

嗯,你能解释一下协变和逆变的目标吗?它可能会帮助我更多地理解它。 看最后一位,这是编译器之前阻止的,in和out的目的是说你可以用安全的接口(或类型)做什么,让编译器赢不要阻止你做安全的事情。 精湛的答案,我看了视频,他们非常有帮助,结合你的例子,我现在已经整理好了。只剩下一个问题,那就是为什么需要“出”和“入”,为什么 Visual Studio 不自动知道您要做什么(或背后的原因是什么)? Automagic “我知道你在那儿做什么”通常在声明类之类的东西时不受欢迎,最好让程序员明确标记类型。您可以尝试将“in”添加到具有返回 T 的方法的类中,编译器会抱怨。想象一下,如果它只是默默地删除了之前自动为您添加的“in”会发生什么。 如果一个关键字需要这么长的解释,那显然是有问题的。在我看来,C# 在这种特殊情况下试图变得过于聪明。尽管如此,还是感谢您的精彩解释。【参考方案2】:

This post 是我读过的最好的主题

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

【讨论】:

链接似乎已失效。这是一个存档版本:web.archive.org/web/20140626123445/http://adamnathan.co.uk/… 我更喜欢这个解释:codepureandsimple.com/…

以上是关于了解 C# 中的协变和逆变接口的主要内容,如果未能解决你的问题,请参考以下文章

Typescript 中的协变和逆变

为啥 C# (4.0) 不允许泛型类类型中的协变和逆变?

c#泛型的协变和逆变

Typescript中的协变和逆变

Typescript中的协变和逆变

Typescript中的协变和逆变