Question About C# Covariance

Question about C# covariance

Simply put, IList<T> is not covariant, whereas IEnumerable<T> is. Here's why...

Suppose IList<T> was covariant. The code below is clearly not type-safe... but where would you want the error to be?

IList<Apple> apples = new List<Apple>();
IList<Fruit> fruitBasket = apples;
fruitBasket.Add(new Banana()); // Aargh! Added a Banana to a bunch of Apples!
Apple apple = apples[0]; // This should be okay, but wouldn't be

For lots of detail on variance, see Eric Lippert's blog post series on it, or watch the video of my talk about variance from NDC.

Basically, variance is only ever allowed where it's guaranteed to be safe (and in a representation-preserving way, which is why you can't convert IEnumerable<int> into IEnumerable<object> - the boxing conversion doesn't preserve representation).

C# covariance confusion

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, C.X 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.

C# dont understand covariance and contravariance of delegates

Following the comments under the question, the Variance in Delegates document will explain that

.NET Framework 3.5 introduced variance support for matching method
signatures with delegate types in all delegates in C#. This means that
you can assign to delegates not only methods that have matching
signatures, but also methods that return more derived types
(covariance) or that accept parameters that have less derived types
(contravariance) than that specified by the delegate type.

So, your assignment MyDelegate<Dog, Mammal> myDelegate = TestMethod; is perfectly fine, despite different signatures on delegate and TestMethod (reversed input parameter and return type).

But in and out parameters are still needed when you have an implicit conversion between delegates, according to Variance in Generic Type Parameters section

To enable implicit conversion, you must explicitly declare generic
parameters in a delegate as covariant or contravariant by using the in
or out keyword.

E.g. the following code will not compile

MyDelegate<Dog, Mammal> myDelegate = TestMethod;
MyDelegate<Dog, Dog> anotherDelegate = TestMethod;
myDelegate = anotherDelegate; //error CS0029: Cannot implicitly convert type...

Until you declare MyDelegate witn contravariant parameter and covariant return type

delegate TOut MyDelegate<in TIn, out TOut>(TIn input);

After that last line will compile

C# variance problem: Assigning ListDerived as ListBase

Well this certainly won't be supported in C# 4. There's a fundamental problem:

List<Giraffe> giraffes = new List<Giraffe>();
giraffes.Add(new Giraffe());
List<Animal> animals = giraffes;
animals.Add(new Lion()); // Aargh!

Keep giraffes safe: just say no to unsafe variance.

The array version works because arrays do support reference type variance, with execution time checking. The point of generics is to provide compile-time type safety.

In C# 4 there will be support for safe generic variance, but only for interfaces and delegates. So you'll be able to do:

Func<string> stringFactory = () => "always return this string";
Func<object> objectFactory = stringFactory; // Safe, allowed in C# 4

Func<out T> is covariant in T because T is only used in an output position. Compare that with Action<in T> which is contravariant in T because T is only used in an input position there, making this safe:

Action<object> objectAction = x => Console.WriteLine(x.GetHashCode());
Action<string> stringAction = objectAction; // Safe, allowed in C# 4

IEnumerable<out T> is covariant as well, making this correct in C# 4, as pointed out by others:

IEnumerable<Animal> animals = new List<Giraffe>();
// Can't add a Lion to animals, as `IEnumerable<out T>` is a read-only interface.

In terms of working around this in your situation in C# 2, do you need to maintain one list, or would you be happy creating a new list? If that's acceptable, List<T>.ConvertAll is your friend.

Covariance and contravariance real world example

Let's say you have a class Person and a class that derives from it, Teacher. You have some operations that take an IEnumerable<Person> as the argument. In your School class you have a method that returns an IEnumerable<Teacher>. Covariance allows you to directly use that result for the methods that take an IEnumerable<Person>, substituting a more derived type for a less derived (more generic) type. Contravariance, counter-intuitively, allows you to use a more generic type, where a more derived type is specified.

See also Covariance and Contravariance in Generics on MSDN.

Classes:

public class Person 
{
public string Name { get; set; }
}

public class Teacher : Person { }

public class MailingList
{
public void Add(IEnumerable<out Person> people) { ... }
}

public class School
{
public IEnumerable<Teacher> GetTeachers() { ... }
}

public class PersonNameComparer : IComparer<Person>
{
public int Compare(Person a, Person b)
{
if (a == null) return b == null ? 0 : -1;
return b == null ? 1 : Compare(a,b);
}

private int Compare(string a, string b)
{
if (a == null) return b == null ? 0 : -1;
return b == null ? 1 : a.CompareTo(b);
}
}

Usage:

var teachers = school.GetTeachers();
var mailingList = new MailingList();

// Add() is covariant, we can use a more derived type
mailingList.Add(teachers);

// the Set<T> constructor uses a contravariant interface, IComparer<in T>,
// we can use a more generic type than required.
// See https://msdn.microsoft.com/en-us/library/8ehhxeaf.aspx for declaration syntax
var teacherSet = new SortedSet<Teachers>(teachers, new PersonNameComparer());

still confused about covariance and contravariance & in/out

Both covariance and contravariance in C# 4.0 refer to the ability of using a derived class instead of base class. The in/out keywords are compiler hints to indicate whether or not the type parameters will be used for input and output.

Covariance

Covariance in C# 4.0 is aided by out keyword and it means that a generic type using a derived class of the out type parameter is OK. Hence

IEnumerable<Fruit> fruit = new List<Apple>();

Since Apple is a Fruit, List<Apple> can be safely used as IEnumerable<Fruit>

Contravariance

Contravariance is the in keyword and it denotes input types, usually in delegates. The principle is the same, it means that the delegate can accept more derived class.

public delegate void Func<in T>(T param);

This means that if we have a Func<Fruit>, it can be converted to Func<Apple>.

Func<Fruit> fruitFunc = (fruit)=>{};
Func<Apple> appleFunc = fruitFunc;

Why are they called co/contravariance if they are basically the same thing?

Because even though the principle is the same, safe casting from derived to base, when used on the input types, we can safely cast a less derived type (Func<Fruit>) to a more derived type (Func<Apple>), which makes sense, since any function that takes Fruit, can also take Apple.

Covariance issue

If you can use C# 4.0, you can write the following code.

class A { }
class B : A {}

interface IC<out T> {}
class C<T> :IC<T> where T : A { protected T property { get; set; } }

class D : C<B> {}

class MainClass {
public static void Main()
{
IC<A> x = new D();
}
}

covariance in c#

You can't do this, because it wouldn't be safe. Consider:

List<Joe> joes = GetJoes();    
List<Human> humanJoes = joes;
humanJoes.Clear();
humanJoes.Add(new Fred());
Joe joe = joes[0];

Clearly the last line (if not an earlier one) has to fail - as a Fred isn't a Joe. The invariance of List<T> prevents this mistake at compile time instead of execution time.

covariance/contravariance problem in C# generic delegate

This question illustrates some interesting facts about contravariance and covariance.

There are two ways to understand these problems. The first is to look at it abstractly and just look at "what direction the arrows go".

Remember that "covariance" means that a transformation preserves the direction of the assignability arrow and "contravariance" means it is reversed. That is, if A --> B means "An object of type A can be assigned to a variable of type B", then:

Giraffe --> Animal
IEnumerable<Giraffe> --> IEnumerable<Animal>
IComparable<Giraffe> <-- IComparable<Animal>

Making a sequence preserves the direction of the arrow; it is "co-variant". "Co" meaning "going with" here. Making a comparison reverses the direction, it is "contra", meaning "going against".

This should make sense; a sequence of giraffes can be used where a sequence of animals is needed. And if you have a thing that can compare any animals, then it can compare any giraffes.

The way to understand why your last two program fragments are both legal is because in the case where you have two nested covariant types, you are saying "go the same direction, then go the same direction as that", which is the same as "go the same direction". When you nest two contravariant types, you are saying "go the opposite direction, then go the opposite direction as that", which is the same as "go the same direction"! Contravariance reverses the direction of an arrow. Reversing the arrow twice turns it back the way it was facing originally!

But that is not how I like to understand these things. Rather, I like to think about the question "what could go wrong if we did it the other way?"

So let's look at your four cases and ask "what can go wrong"?

I'll make some small changes to your types.

public delegate void D1<in T>(T t);
public delegate void D2<in T>(D1<T> d1t); // This is wrong.

Why is D2 wrong? Well, what can go wrong if we allowed it?

// This is a cage that can hold any animal.
AnimalCage cage = new AnimalCage();
// Let's make a delegate that inserts an animal into the cage.
D1<Animal> d1animal = (Animal a) => cage.InsertAnimal(a);
// Now lets use the same delegate to insert a tiger. That's fine!
D1<Tiger> d1tiger = d1animal;
d1tiger(new Tiger());

Now there is a tiger in cage, which is fine; the cage can hold any animal.

But now let's see how things go wrong with D2. Let's suppose that the declaration of D2 was legal.

// This line is fine; we're assigning D1<Animal> to D1<Tiger> 
// and it is contravariant.
D2<Animal> d2animal = (D1<Animal> d1a) => {d1tiger = d1a;};
// An aquarium can hold any fish.
Aquarium aquarium = new Aquarium();
// Let's make a delegate that puts a fish into an aquarium.
D1<Fish> d1fish = (Fish f) => aquarium.AddFish(f);
// This conversion is fine, because D2 is contravariant.
D2<Fish> d2fish = d2animal;
// D2<Fish> takes a D1<Fish> so we should be able to do this:
d2fish(d1fish);
// Lets put another tiger in the cage.
d1tiger(new Tiger());

OK, every line in that program was type safe. But trace through the logic. What happened? When we called d1tiger on the last line, what did it equal? Well, d2fish(d1fish) assigns d1fish to... d1tiger. But d1tiger is typed as D1<Tiger> not D1<Fish>. So we've assigned a value to a variable of the wrong type. Then what happened? We called d1Tiger with a new tiger, and d1Tiger put a tiger into an aquarium!

Every one of those lines was typesafe, but the program was not typesafe, so what should we conclude? The declaration of D2 was not typesafe. And that's why the compiler gives you an error.

Based on this analysis we know that D2<in T>(D1<T>) has to be wrong.

Exercise 1:

delegate T D3<out T>();
delegate void D4<in T>(D3<T> d3t);

Go through the same logic I did, but this time, convince yourself that this never gives rise to a type system problem.

Once you've got that down, then do the hard ones:

Exercise 2: Go through the logic again, but this time with

delegate void D5<in T>(D3<D3<T>> d3d3t);

Again, convince yourself that this is legal, and that this case is logically the same as Exercise 1.

Exercise 3: And the last, hardest one is:

delegate void D6<in T>(D1<D1<T>> d1d1t);

Convince yourself that this is legal because D1<D1<T>> reverses the arrow twice, and is therefore logically the same as Exercise 1.



Related Topics



Leave a reply



Submit