简体   繁体   中英

C# covariance confusion

The following is a code snippet about covariance in 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. 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 >. But IExtract<string> simply does NOT have a method named Extract() that it inherits from IExtract<object> , unlike the normal inheritance. 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? 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. 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.

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."

When we convert c to f, c and f have exactly the same content . They are the same reference . We have just verified that c is of a type that has a slot table compatible with IFoo.

Now let's go through your post.

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

Let's crisp that up.

We have sampleClassOfString which implements IExtract<string> . Its type has a "slot table" that says "my Extract goes in the slot for IExtract<string>.Extract ".

Now, when sampleClassOfString is converted to IExtract<object> , again, we have to make a check. Does sampleClassOfString 's type contain an interface slot table that is suitable for IExtract<object> ? Yes it does: we can use the existing table for IExtract<string> for that purpose .

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 . Well, the method that is in the IExtract<string>.Extract slot meets that contract; 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. The assignment succeeds, and all invocations will go through the IExtract<string> slot table.

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

Correct.

except the fact that C# made IExtract<string> assignable to 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.

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. 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>

That's correct. There is no inheritance whatsoever between IExtract<string> and 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 . 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?

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. There is absolutely nothing whatsoever "hackish" about it.

so exactly what happens when I invoke 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> .

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> . 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.

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. 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

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:

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.

A bunch more articles, in "newest first" chronological order, can be found here:

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

and a few here:

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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