C#协方差混淆

时间:2019-01-18 15:30:57

标签: c# covariance

以下是有关C#中协方差的代码片段。我对如何应用协方差有一定的了解,但是我很难掌握一些详细的技术知识。

using System;
namespace CovarianceExample
{
    interface IExtract<out T> { T Extract(); }
    class SampleClass<T> : IExtract<T>
    {
        private T data;
        public SampleClass(T data) {this.data = data;}   //ctor
        public T Extract()                               // Implementing interface
        {
            Console.WriteLine
                ("The type where the executing method is declared:\n{0}",
                this.GetType() );
            return this.data;
        }
    }
    class CovarianceExampleProgram
    {
        static void Main(string[] args)
        {
            SampleClass<string> sampleClassOfString = new SampleClass<string>("This is a string");
            IExtract<Object> iExtract = sampleClassOfString;

            // IExtract<object>.Extract() mapes to IExtract<string>.Extract()?
            object obj = iExtract.Extract();
            Console.WriteLine(obj);                  
            Console.ReadKey();
        }
    }
}

// Output:
// The type where the executing method is declared:
// CovarianceExample.SampleClass`1[System.String]
// This is a string

调用IExtract<object>.Extract()会调用IExtract<string>.Extract(),如输出所示。尽管我有点期待这种行为,但我无法告诉自己为什么它会像以前那样行为。

IExtract<object>在包含NOT的继承层次结构中为IExtract<string>,但C#使IExtract<string>可分配给IExtract<object>的事实除外。但是IExtract<string>只是没有拥有一种从Extract()继承的名为IExtract<object>的方法,与 normal 继承不同。目前,这对我来说似乎意义不大。

IExtract<string> OWN 巧合(或设计使然)类似命名为Extract()的方法隐藏IExtract<object>的{{ 1}}方法?那是一种黑客吗? (错误的单词选择!)

谢谢

1 个答案:

答案 0 :(得分:12)

对于协方差的工作原理,您肯定有一些严重的误解,但我尚不清楚100%是什么。首先让我说说接口是什么,然后我们可以逐行讨论您的问题并指出所有误解。

将接口视为“插槽”的集合,其中每个插槽都有一个 contract ,并且包含一种能实现该合约的方法。例如,如果我们有:

interface IFoo { Mammal X(Mammal y); }

然后IFoo有一个插槽,并且该插槽必须包含一种将哺乳动物带回并返回哺乳动物的方法。

当我们将引用隐式或显式转换为接口类型时,我们不会以任何方式更改引用 。相反,我们验证,所引用的类型已经具有该接口的有效插槽表。因此,如果我们有:

class C : IFoo { 
  public Mammal X(Mammal y)
  {
    Console.WriteLine(y.HairColor);
    return new Giraffe();
  }
}

后来

C c = new C();
IFoo f = c;

认为C有一张小表,上面写着:“如果C转换为IFoo,则C.X进入IFoo.X插槽。”

当我们将c转换为f时,c和f的内容完全相同。它们是相同的引用。我们已经验证,即c的类型具有与IFoo兼容的插槽表。

现在让我们浏览一下您的帖子。

  

调用IExtract<object>.Extract()会调用IExtract<string>.Extract(),如输出所示。

让我们整理一下。

我们有sampleClassOfString实现了IExtract<string>。它的类型有一个“插槽表”,上面写着“我的Extract进入插槽IExtract<string>.Extract”。

现在,当sampleClassOfString转换为IExtract<object>时,同样,我们必须进行检查。 sampleClassOfString的类型是否包含适合IExtract<object>的接口插槽表?是的,它确实如此:为此,我们可以将现有表用于IExtract<string>

即使它们是两种不同的类型,我们为什么也可以使用它呢?因为所有合同仍得到履行

IExtract<object>.Extract有合同:这是什么都不做并返回object的方法。好吧,IExtract<string>.Extract插槽中的方法符合该合同;它什么也不做,它返回一个字符串,它是一个对象。

既然所有合同都得到满足,我们可以使用已经获得的IExtract<string>槽位表。分配成功,所有调用将通过IExtract<string>插槽表。

  

IExtract<object>不在包含IExtract<string>

的继承层次结构中

正确。

  

除了C#使IExtract<string>可分配给IExtract<object>的事实。

不要混淆这两件事;它们不一样。 继承基本类型的成员也是派生类型的成员的属性。 分配兼容性是一种属性,可以将一种类型的实例分配给另一种类型的变量。从逻辑上说,这些是完全不同的!

是的,存在某种联系,只要派生意味着赋值兼容性和继承;如果D是基本类型B的派生类型,则D的实例可分配给类型B的变量,并且 B的所有可继承成员都是D的成员。

但是不要混淆这两件事。仅仅因为它们是相关的并不意味着它们是相同的。实际上,有些语言是不同的。也就是说,有些语言的继承与分配兼容性正交。 C#并不是其中之一,并且您已经习惯了这样一个世界,在这个世界中,继承和赋值兼容性是如此紧密地联系在一起,您从未学会将它们视为独立的。开始将它们视为不同的事物,因为它们确实如此。

协方差就是将赋值兼容性关系扩展到不在继承层次结构中的类型。这就是协方差的意思;如果该分配兼容性关系是在映射到泛型的映射中保留的,则该分配兼容性关系为 covariant 。 “ 协方差”表示:“在需要水果的地方可以使用苹果;因此,在需要水果的地方可以使用一系列苹果”。在映射到序列的过程中保留了分配兼容性关系

  

但是IExtract<string>根本没有从Extract()继承的名为IExtract<object>的方法

是的。 IExtract<string>IExtract<object>之间没有任何继承。但是,它们之间存在 compatibility 关系,因为符合Extract约定的任何方法 IExtract<string>.Extract也是 >符合IExtract<object>.Extract约定的方法。因此,前者的插槽表可以在需要后者的情况下使用。

  

IExtract<string>的OWN巧合地(或通过设计)类似地命名为Extract()的方法掩盖IExtract<object>的Extract()方法是否明智?

绝对不是。没有任何隐藏。当派生类型的成员与基本类型的继承成员具有相同的名称,并且新成员隐藏旧的以便在编译时查找名称时,就会发生“隐藏”。隐藏只是一个编译时名称查找概念;它与接口在运行时的工作方式无关。

  

那是一种黑客吗?

绝对不是

我试图不使人觉得攻势,而且大多是成功的。 :-)

此功能是由专家精心设计的;它是合理的(以模数形式扩展到C#中现有的不健全度,例如不安全的数组协方差),并且在实施时要格外谨慎。绝对没有任何“骇客”。

  

那么当我调用IExtract<object>.Extract()时会发生什么呢?

从逻辑上讲,这会发生:

将类引用转换为IExtract<object>时,我们验证引用中是否存在与IExtract<object>兼容的插槽表。

当您调用Extract时,我们在已确定与Extract兼容的插槽表中查找IExtract<object>插槽的内容。由于这是与对象IExtract<string>已有的插槽表 相同的东西,所以发生了同样的事情:类的Extract方法在该插槽中,因此它被调用

实际上,情况要复杂得多;在通常情况下,调用逻辑中有一堆齿轮可以确保良好的性能。但是从逻辑上讲,您应该将其视为在表中找到一个方法,然后调用该方法。

  

代表也可以标记为协变和反变。如何运作?

从逻辑上讲,您可以将委托视为仅具有一个称为“ Invoke”的方法的接口,然后从那里开始。实际上,由于代表组成等原因,机制当然有所不同,但也许现在您可以看到它们如何工作。

  

我在哪里可以了解更多信息?

这有点费劲:

https://stackoverflow.com/search?q=user%3A88656+covariance

所以我将从顶部开始:

Difference between Covariance & Contra-variance

如果要使用C#4.0中的功能历史记录,请从此处开始:

https://blogs.msdn.microsoft.com/ericlippert/2007/10/16/covariance-and-contravariance-in-c-part-one/

请注意,这是在我们将“ in”和“ out”确定为反方差和协方差的关键字之前编写的。

在这里可以找到按“最新时间先后”顺序排列的更多文章:

https://blogs.msdn.microsoft.com/ericlippert/tag/covariance-and-contravariance/

还有一些在这里:

https://ericlippert.com/category/covariance-and-contravariance/


练习:现在您大致了解了它在幕后的工作原理,您认为这样做是什么?

interface IFrobber<out T> { T Frob(); }
class Animal { }
class Zebra: Animal { }
class Tiger: Animal { }
// Please never do this:
class Weird : IFrobber<Zebra>, IFrobber<Tiger>
{
  Zebra IFrobber<Zebra>.Frob() => new Zebra();
  Tiger IFrobber<Tiger>.Frob() => new Tiger();
}
…
IFrobber<Animal> weird = new Weird();
Console.WriteLine(weird.Frob());

?考虑一下,看看是否可以解决。