简体   繁体   English

C#协方差混淆

[英]C# covariance confusion

The following is a code snippet about covariance in C#. 以下是关于C#协方差的代码片段。 I have some understanding about how to apply covariance, but there is some detailed technical stuff that I have hard time grasping. 我对如何应用协方差有一些了解,但有一些我很难掌握的详细技术内容。

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

Invoking IExtract<object>.Extract() invokes IExtract<string>.Extract() , as evidenced by the output. 调用IExtract<object>.Extract()调用IExtract<string>.Extract() ,如输出所示。 While I kind of expected this behavior, I am not able to tell myself why it behaved the way it did. 虽然我有点期待这种行为,但我无法告诉自己为什么它的行为方式如此。

IExtract<object> is NOT in the inheritance hierarchy containing IExtract<string> , except the fact that C# made IExtract<string> assignable to IExtract<object >. IExtract<object>NOT在包含继承层次IExtract<string> ,除了以下事实:C#制成IExtract<string>分配给IExtract<object >。 But IExtract<string> simply does NOT have a method named Extract() that it inherits from IExtract<object> , unlike the normal inheritance. IExtract<string>根本没有一个叫方法Extract()它继承自IExtract<object>不像正常的继承。 It doesn't appear to make much sense to me at this time. 这个时候对我来说似乎没有多大意义。

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? 是否明智地说IExtract<string>OWN巧合(或设计)同样名为Extract()方法隐藏了IExtract<object >的Extract()方法? And that it is a kind of a hack? 而且这是一种黑客攻击? (Bad choice of word!) (选择不好的词!)

Thanks 谢谢

You definitely have some serious misunderstanding about how covariance works, but it's not 100% clear to me what it is. 你肯定对协方差如何运作有一些严重的误解,但它并不是100%清楚我是什么。 Let me first say what interfaces are, and then we can go through your question line by line and point out all the misunderstandings. 我先说一下接口是什么,然后我们可以逐行查看你的问题并指出所有的误解。

Think of an interface as a collection of "slots", where each slot has a contract , and contains a method that fulfils that contract. 将接口视为“插槽”的集合,其中每个插槽都有一个合同 ,并包含一个满足该合同的方法。 For example, if we have: 例如,如果我们有:

interface IFoo { Mammal X(Mammal y); }

then IFoo has a single slot, and that slot must contain a method that takes a mammal and returns a mammal. 那么IFoo只有一个插槽,那个插槽必须包含一个接收哺乳动物并返回哺乳动物的方法。

When we implicitly or explicitly convert a reference to an interface type, we do not change the reference in any way . 当我们隐式或显式地将引用转换为接口类型时,我们不会以任何方式更改引用。 Rather, we verify that the referred-to type already has a valid slot table for that interface. 相反,我们验证引用类型是否具有该接口的有效槽表。 So if we have: 所以如果我们有:

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

And later 然后

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

Think of C as having a little table that says "if a C is converted to IFoo, CX goes in the IFoo.X slot." 可以认为C有一个小表,表示“如果C转换为IFoo,则CX进入IFoo.X插槽。”

When we convert c to f, c and f have exactly the same content . 当我们将c转换为f时,c和f具有完全相同的内容 They are the same reference . 它们是相同的参考 We have just verified that c is of a type that has a slot table compatible with IFoo. 我们刚刚验证了c是一种具有与IFoo兼容的槽表的类型。

Now let's go through your post. 现在让我们来看看你的帖子。

Invoking IExtract<object>.Extract() invokes IExtract<string>.Extract() , as evidenced by the output. 调用IExtract<object>.Extract()调用IExtract<string>.Extract() ,如输出所示。

Let's crisp that up. 让我们清醒一点。

We have sampleClassOfString which implements IExtract<string> . 我们有sampleClassOfString ,它实现了IExtract<string> Its type has a "slot table" that says "my Extract goes in the slot for IExtract<string>.Extract ". 它的类型有一个“槽表”,表示“我的Extract进入IExtract<string>.Extract的插槽”。

Now, when sampleClassOfString is converted to IExtract<object> , again, we have to make a check. 现在,当sampleClassOfString转换为IExtract<object>时,我们还要进行检查。 Does sampleClassOfString 's type contain an interface slot table that is suitable for IExtract<object> ? sampleClassOfString的类型是否包含适用于IExtract<object>的接口槽表? Yes it does: we can use the existing table for IExtract<string> for that purpose . 是的确如此: 我们可以将现有的表用于IExtract<string>

Why can we use it, even though those are two different types? 为什么我们可以使用它,即使它们是两种不同的类型? Because all the contracts are still met . 因为所有合同仍然符合

IExtract<object>.Extract has a contract: it is a method that takes nothing and returns object . IExtract<object>.Extract有一个契约:它是一个不带任何东西并返回object Well, the method that is in the IExtract<string>.Extract slot meets that contract; 那么, IExtract<string>.Extract插槽中的方法符合该合同; it takes nothing, and it returns a string, which is an object. 它不需要任何东西,它返回一个字符串,这是一个对象。

Since all the contracts are met, we can use the IExtract<string> slot table we've already got. 由于满足了所有合同,我们可以使用我们已经获得的IExtract<string>槽表。 The assignment succeeds, and all invocations will go through the IExtract<string> slot table. 赋值成功,所有调用都将通过IExtract<string>槽表。

IExtract<object> is NOT in the inheritance hierarchy containing IExtract<string> IExtract<object> IExtract<string>包含IExtract<string>的继承层次结构中

Correct. 正确。

except the fact that C# made IExtract<string> assignable to IExtract<object> . 除了C#使IExtract<string>可分配给IExtract<object>这一事实。

Don't confuse those two things; 不要混淆这两件事; they are not the same. 他们不一样。 Inheritance is the property that a member of a base type is also a member of a derived type . 继承基类型的成员也是派生类型的成员的属性。 Assignment compatibility is the property that an instance of one type may be assigned to a variable of another type. 赋值兼容性是可以将一种类型的实例分配给另一种类型的变量的属性。 Those are logically very different! 那些在逻辑上非常不同!

Yes, there is a connection, insofar that derivation implies both assignment compatibility and inheritance ; 是的,存在连接,因为推导意味着赋值兼容性和继承性 ; if D is a derived type of base type B then an instance of D is assignable to a variable of type B, and all heritable members of B are members of D. 如果D是基类型B的派生类型,那么D的实例可分配给类型B的变量, B的所有可遗传成员都是D.的成员。

But don't confuse those two things; 但不要混淆这两件事; just because they are related does not mean they are the same. 仅仅因为它们是相关的并不意味着它们是相同的。 There are actually languages where they are different; 实际上有些语言不同; that is, there are languages where inheritance is orthogonal to assignment compatibility. 也就是说,存在继承与赋值兼容性正交的语言。 C# just is not one of them, and you're so used to a world where inheritance and assignment compatibility are so closely linked you've never learned to see them as separate. C#只是不是其中之一,你已经习惯了这样一个世界,在这个世界中,继承和赋值兼容性是如此紧密地联系在一起,你从未学会将它们视为独立的。 Start thinking of them as different things, because they are. 开始将它们视为不同的东西,因为它们是。

Covariance is about extending the assignment compatibility relation to types which are not in inheritance hierarchies. 协方差是指将赋值兼容性关系扩展到不在继承层次结构中的类型。 That's what covariance means ; 这就是协方差的意思 ; the assignment compatibility relation is covariant if the relation is preserved across a mapping to a generic . 如果在映射到泛型的过程中保留关系,则赋值兼容性关系是协变 "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" is covariance . “可以在需要水果的地方使用苹果;因此,在需要一系列水果的情况下,可以使用一系列苹果”是协方差 The assignment compatibility relationship is preserved across the mapping to sequences . 在映射到序列之间保留赋值兼容性关系。

But IExtract<string> simply does NOT have a method named Extract() that it inherits from IExtract<object> IExtract<string>根本没有一个名为Extract()的方法,它继承自IExtract<object>

That's correct. 那是对的。 There is no inheritance whatsoever between IExtract<string> and IExtract<object> . IExtract<string>IExtract<object>之间没有任何继承 However, there is a compatibility relationship between them, because any method Extract which meets the contract of IExtract<string>.Extract is also a method that meets the contract of IExtract<object>.Extract . 但是,它们之间存在兼容性关系,因为符合IExtract<string>.Extract合同的任何方法 Extract 也是满足IExtract<object>.Extract合同的方法。 Therefore, the slot table of the former may be used in a situation requiring the latter. 因此,前者的时隙表可以在需要后者的情况下使用。

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? 是否明智地说IExtract<string>的OWN巧合(或设计)同样名为Extract()方法隐藏了IExtract<object>的Extract()方法?

Absolutely not. 绝对不。 There is no hiding whatsoever. 没有任何隐藏。 "Hiding" occurs when a derived type has a member of the same name as an inherited member of a base type, and the new member hides the old one for the purposes of looking up names at compile time . 当衍生类型具有与基类型的继承成员同名的成员时,会发生“隐藏”,并且新成员会隐藏旧成员以便在编译时查找名称 Hiding is solely a compile-time name lookup concept; 隐藏只是一个编译时名称查找概念; it has nothing whatsoever to do with how interfaces work at runtime. 它与接口在运行时的工作方式没有任何关系。

And that it is a kind of a hack? 而且这是一种黑客攻击?

ABSOLUTELY NOT . 绝对没有

I'm trying to not find the suggestion offensive, and mostly succeeding. 我试图找不到令人反感的建议,而且大多数都是成功的。 :-) :-)

This feature was carefully designed by experts; 此功能由专家精心设计; it is sound (modulo extending to existing unsoundnesses in C#, such as unsafe array covariance), and it was implemented with a great deal of caution and review. 它是合理的(模数扩展到C#中现有的不健全性,例如不安全的数组协方差),并且它的实施非常谨慎和审查。 There is absolutely nothing whatsoever "hackish" about it. 绝对没有任何“hackish”它。

so exactly what happens when I invoke IExtract<object>.Extract() ? 那么当我调用IExtract<object>.Extract()时会发生什么?

Logically, this is what happens: 从逻辑上讲,这是发生的事情:

When you convert the class reference to IExtract<object> , we verify that there is a slot table in the reference that is compatible with IExtract<object> . 将类引用转换为IExtract<object> ,我们验证引用中是否存在与IExtract<object>兼容的槽表。

When you invoke Extract , we look up the contents of the Extract slot in the slot table we have identified as compatible with IExtract<object> . 当您调用Extract ,我们在已确定与IExtract<object>兼容的槽表中查找Extract槽的内容。 Since that is the same slot table as the one the object already has for IExtract<string> , the same thing happens: the class's Extract method is in that slot, so it gets invoked. 因为这是 IExtract<string>对象相同的槽表 ,所以同样的事情发生了:类的Extract方法在那个槽中,所以它被调用。

In practice, the situation is a little more complicated than that; 在实践中,情况稍微复杂一点; there is a bunch of gear in the invocation logic that is there to ensure good performance in common cases. 在调用逻辑中有一堆齿轮可以确保在常见情况下的良好性能。 But logically, you should think of it as finding a method in a table, and then invoking that method. 但从逻辑上讲,您应该将其视为在表中查找方法,然后调用该方法。

Delegates can also be marked as covariant and contravariant. 代表也可以被标记为协变和逆变。 How does that work? 这是如何运作的?

Logically, you can think of delegates as just interfaces that have a single method called "Invoke", and it follows from there. 从逻辑上讲,您可以将委托视为只有一个名为“Invoke”的方法的接口,并从那里开始。 In practice, of course the mechanisms are somewhat different thanks to things like delegate composition, but perhaps now you can see how they might work. 实际上,由于代理组合等问题,机制当然有所不同,但现在您可以看到它们的工作方式。

Where can I learn more? 我在哪里可以了解更多?

This is a bit of a firehose: 这有点像消防:

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

so I would start at the top: 所以我会从顶部开始:

Difference between Covariance & Contra-variance 协方差和对比方差之间的差异

If you want the history of the feature in C# 4.0, start here: 如果您想要C#4.0中的功能历史记录,请从这里开始:

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

Note that this was written before we had settled on "in" and "out" as the keywords for contravariance and covariance. 请注意,这是在我们确定“in”和“out”作为逆变和协方差的关键词之前编写的。

A bunch more articles, in "newest first" chronological order, can be found here: 在这里可以找到更多以“最新的第一”时间顺序排列的文章:

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

and a few here: 还有一些在这里:

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


EXERCISE: Now that you know roughly how this works behind the scenes, what do you think this does? 练习:现在您已经大致了解了幕后的工作方式,您认为这有什么作用?

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

? Give it some thought, and see if you can work out what happens. 仔细考虑一下,看看你是否能解决所发生的事情。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM