以下是有关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}}方法?那是一种黑客吗? (错误的单词选择!)
谢谢
答案 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());
?考虑一下,看看是否可以解决。