C# 协方差混淆

C# covariance confusion

以下是关于 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> 在包含 IExtract<string> 的继承层次结构中是 NOT,除了 C# 使 IExtract<string> 可分配给 IExtract<object>。但是 IExtract<string> 只是 NOT 有一个名为 Extract() 的方法,它继承自 IExtract<object>,不像 normal[=39= 】 传承。目前对我来说似乎没有多大意义。

如果说 IExtract<string>OWN 巧合(或设计)类似地命名为 Extract() 方法隐藏 IExtract<object 是否明智> 的 Extract() 方法?这是一种黑客攻击? (选词错误!)

谢谢

你肯定对协方差的工作原理有一些严重的误解,但我不是 100% 清楚它是什么。我先说什么是接口,然后我们逐行分析你的问题,指出所有的误解。

将接口视为 "slots" 的集合,其中每个插槽都有一个 合同 ,并且 包含 一个方法履行该合同。例如,如果我们有:

interface IFoo { Mammal X(Mammal y); }

然后 IFoo 有一个插槽,该插槽必须包含一个接受哺乳动物和 returns 哺乳动物的方法。

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

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

以后

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

认为 C 有一点 table 表示 "if a C is converted to IFoo, C.X goes in the IFoo.X slot."

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

现在让我们检查一下您的 post。

Invoking IExtract<object>.Extract() invokes IExtract<string>.Extract(), as evidenced by the output.

让我们把它弄清楚。

我们有 sampleClassOfString 实现了 IExtract<string>。它的类型有一个 "slot table" 表示 "my Extract goes in the slot for IExtract<string>.Extract".

现在,当 sampleClassOfString 转换为 IExtract<object> 时,我们必须再次进行检查。 sampleClassOfString 的类型是否包含接口插槽 table,即 suitable for IExtract<object>?是的,确实如此:我们可以将现有的 table 用于 IExtract<string> 用于此目的

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

IExtract<object>.Extract有一个契约:它是一个不带任何东西的方法和returnsobject。好吧,IExtract<string>.Extract 插槽中的方法符合该约定;它不需要任何东西,它 returns 一个字符串,它是一个对象。

既然满足了所有的契约,我们就可以使用我们已经得到的IExtract<string>插槽table。赋值成功,所有调用都会通过IExtract<string>槽table.

IExtract<object> is NOT in the inheritance hierarchy containing IExtract<string>

正确。

except the fact that C# made IExtract<string> assignable to IExtract<object>.

不要混淆这两者;它们不一样。 继承是属性,即基类型的成员也是派生类型的成员赋值兼容性是属性一种类型的实例可以赋给另一种类型的变量。这些在逻辑上是非常不同的!

是的,有联系,因为派生意味着赋值兼容性和继承;如果 D 是基类型 B 的派生类型,则 D 的实例可分配给类型 B 的变量, B 的所有 heritable 成员都是 D 的成员。

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

协变是将赋值兼容性关系扩展到不在继承层次结构中的类型。这就是协变的意思;分配兼容性关系是 协变 如果 关系在到泛型 的映射中被保留。 "An apple may be used where a fruit is needed; therefore a sequence of apples may be used where a sequence of fruits is needed" 是 协方差 。分配兼容性关系 在到序列 .

的映射中保留

But IExtract<string> simply does NOT have a method named Extract() that it inherits from IExtract<object>

没错。在IExtract<string>IExtract<object>之间没有任何继承。但是,它们之间存在兼容性关系,因为任何方法 Extract满足IExtract<string>.Extract的约定是也是一个符合IExtract<object>.Extract约定的方法。因此,前者的插槽table可能会在需要后者的情况下使用。

Would it be sensible to say that IExtract<string>'s OWN coincidentally (or by design) similarly named Extract() method hides IExtract<object>'s Extract() method?

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

And that it is a kind of a hack?

绝对不会

我试图不觉得这个建议令人反感,而且大部分都成功了。 :-)

此功能由专家精心设计;它是合理的(模扩展到 C# 中现有的不合理性,例如不安全的数组协变),并且它是在非常谨慎和审查的情况下实现的。 "hackish" 完全没有。

so exactly what happens when I invoke IExtract<object>.Extract()?

逻辑上是这样的:

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

当您调用 Extract 时,我们会在我们已确定为与 IExtract<object> 兼容的插槽 table 中查找 Extract 插槽的内容。由于那是 与对象已经具有的 IExtract<string> 相同的插槽 table,同样的事情发生了:class 的 Extract 方法在那个插槽中,因此它被调用。

实际上,情况比那复杂一点;调用逻辑中有一堆工具可以确保在常见情况下的良好性能。但从逻辑上讲,你应该把它看作是在table中找到一个方法,然后调用那个方法。

Delegates can also be marked as covariant and contravariant. How does that work?

从逻辑上讲,您可以将委托视为具有称为 "Invoke" 的单个方法的接口,它从那里开始。在实践中,由于委托组合等因素,机制当然会有所不同,但现在您也许可以了解它们的工作原理。

Where can I learn more?

这有点喷火:

https://whosebug.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" 作为逆变和协变的关键字之前编写的。

更多文章,按"newest first"时间顺序,可以在这里找到:

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());

?想一想,看看你能不能弄清楚会发生什么。