简体   繁体   中英

What are the kinds of covariance in C#? (Or, covariance: by example)

Covariance is (roughly) the ability to mirror inheritance of "simple" types in complex types that use them.
Eg We can always treat an instance of Cat as an instance of Animal . A ComplexType<Cat> may be treated as a ComplexType<Animal> , if ComplexType is covariant.

I'm wondering: what are the "types" of covariance, and how do they relate to C# (are they supported?)
Code examples would be helpful.

For instance, one type is return type covariance , supported by Java, but not C#.

I'm hoping someone with functional programming chops can chime in, too!

Here's what I can think of:

Update

After reading the constructive comments and the ton of articles pointed (and written) by Eric Lippert, I improved the answer:

  • Updated the broken-ness of array covariance
  • Added "pure" delegate variance
  • Added more examples from the BCL
  • Added links to articles that explain the concepts in-depth.
  • Added a whole new section on higher-order function parameter covariance.

Return type covariance :

Available in Java (>= 5) [1] and C++ [2] , not supported in C# (Eric Lippert explains why not and what you can do about it ):

class B {
    B Clone();
}

class D: B {
    D Clone();
}

Interface covariance [3] - supported in C#

The BCL defines the generic IEnumerable interface to be covariant:

IEnumerable<out T> {...}

Thus the following example is valid:

class Animal {}
class Cat : Animal {}

IEnumerable<Cat> cats = ...
IEnumerable<Animal> animals = cats;

Note that an IEnumerable is by definition "read-only" - you can't add elements to it.
Contrast that to the definition of IList<T> which can be modified eg using .Add() :

public interface IEnumerable<out T> : ...  //covariant - notice the 'out' keyword
public interface IList<T> : ...            //invariant

Delegate covariance by means of method groups [4] - supported in C#

class Animal {}
class Cat : Animal {}

class Prog {
    public delegate Animal AnimalHandler();

    public static Animal GetAnimal(){...}
    public static Cat GetCat(){...}

    AnimalHandler animalHandler = GetAnimal;
    AnimalHandler catHandler = GetCat;        //covariance

}

"Pure" delegate covariance [5 - pre-variance-release article] - supported in C#

The BCL definition of a delegate that takes no parameters and returns something is covariant:

public delegate TResult Func<out TResult>()

This allows the following:

Func<Cat> getCat = () => new Cat();
Func<Animal> getAnimal = getCat; 

Array covariance - supported in C#, in a broken way [6] [7]

string[] strArray = new[] {"aa", "bb"};

object[] objArray = strArray;    //covariance: so far, so good
//objArray really is an "alias" for strArray (or a pointer, if you wish)


//i can haz cat?
object cat == new Cat();         //a real cat would object to being... objectified.

//now assign it
objArray[1] = cat                //crash, boom, bang
                                 //throws ArrayTypeMismatchException

And finally - the surprising and somewhat mind-bending
Delegate parameter covariance (yes, that's co -variance) - for higher-order functions. [8]

The BCL definition of the delegate that takes one parameter and returns nothing is contravariant :

public delegate void Action<in T>(T obj)

Bear with me. Let's define a circus animal trainer - he can be told how to train an animal (by giving him an Action that works with that animal).

delegate void Trainer<out T>(Action<T> trainingAction);

We have the trainer definition, let's get a trainer and put him to work.

Trainer<Cat> catTrainer = (catAction) => catAction(new Cat());

Trainer<Animal> animalTrainer = catTrainer;  
// covariant: Animal > Cat => Trainer<Animal> > Trainer<Cat> 

//define a default training method
Action<Animal> trainAnimal = (animal) => 
   { 
   Console.WriteLine("Training " + animal.GetType().Name + " to ignore you... done!"); 
   };

//work it!
animalTrainer(trainAnimal);

The output proves that this works:

Training Cat to ignore you... done!

In order to understand this, a joke is in order.

A linguistics professor was lecturing to his class one day.
"In English," he said, "a double negative forms a positive.
However," he pointed out, "there is no language wherein a double positive can form a negative."

A voice from the back of the room piped up, "Yeah, right."

What's that got to do with covariance?!

Let me attempt a back-of-the-napkin demonstration.

An Action<T> is contravariant, ie it "flips" the types' relationship:

A < B => Action<A> > Action<B> (1)

Change A and B above with Action<A> and Action<B> and get:

Action<A> < Action<B> => Action<Action<A>> > Action<Action<B>>  

or (flip both relationships)

Action<A> > Action<B> => Action<Action<A>> < Action<Action<B>> (2)     

Put (1) and (2) together and we have:

,-------------(1)--------------.
 A < B => Action<A> > Action<B> => Action<Action<A>> < Action<Action<B>> (4)
         `-------------------------------(2)----------------------------'

But our Trainer<T> delegate is effectively an Action<Action<T>> :

Trainer<T> == Action<Action<T>> (3)

So we can rewrite (4) as:

A < B => ... => Trainer<A> < Trainer<B> 

- which, by definition, means Trainer is covariant.

In short, applying Action twice we get contra-contra-variance, ie the relationship between types is flipped twice (see (4) ), so we're back to covariance.

This is best explained in terms of more generic, structural types. Consider:

  1. Tuple types: (T1, T2), a pair of types T1 and T2 (or more generally, n-tuples);
  2. Function types: T1 -> T2, a function with argument type T1 and result T2;
  3. Mutable types: Mut(T), a mutable variable holding a T.

Tuples are covariant in both their component types, ie (T1, T2) < (U1, U2) iff T1 < U1 and T2 < U2 (where '<' means is-subtype-of).

Functions are covariant in their result and contravariant in their argument, ie (T1 -> T2) < (U1 -> U2) iff U1 < T1 and T2 < U2.

Mutable types are invariant, ie Mut(T) < Mut(U) only iff T = U.

All these rules are the most general correct subtyping rules.

Now, an object or interface type like you know it from mainstream languages can be interpreted as a fancy form of tuple containing its methods as functions, among other things. For example, the interface

interface C<T, U, V> {
  T f(U, U)
  Int g(U)
  Mut(V) x
}

essentially represents the type

C(T, U, V) = ((U, U) -> T, U -> Int, Mut(V))

where f, g, and x correspond to the 1st, 2nd, and 3rd component of the tuple, respectively.

It follows from the rules above that C(T, U, V) < C(T', U', V') iff T < T' and U' < U and V = V'. That means that the generic type C is covariant in T, contravariant in U and invariant in V.

Another example:

interface D<T> {
  Int f(T)
  T g(Int)
}

is

D(T) = (T -> Int, Int -> T)

Here, D(T) < D(T') only if T < T' and T' < T. In general, that can only be the case if T = T', so D actually is invariant in T.

There also is a fourth case, sometimes called "bivariance", which means both co- and contravariant at the same time. For example,

interface E<T> { Int f(Int) }

is bivariant in T, because it is not actually used.

Java employs the concept of use-site variance for generic types: the needed variance is specified at each use site. This is why Java programmers are required to be familiar with the so-called PECS rule. Yes, it is unwieldy and has already received plenty of criticism.

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