Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Variance rules in C#

The Exact rules for variance validity are a bit vague and not specific. I'm going to list the rules for what makes a type valid-covariantly, and attach some queries and personal annotations to each of those rules.

A type is valid covariantly if it is:

1) a pointer type, or a non-generic type.

Pointers and non-generic types are not variant in C#, except for arrays and non-generic delegates. Generic classes, structs and enums are invariant. Am I right here?

2) An array type T[] where T is valid covariantly.

So this means that if the element type T of an array T[] is covariant (reference or array element type), then the array is covariant, and if the element type is invariant (value type), then the Array type is invariant. Arrays cannot be contravariant in C#. Am I right here?

3) A generic type parameter type, if it was not declared as being contravariant.

We normally say that a generic type is variant on a parameter type, but for a parameter type to be variant on it's own. Is this another short form for saying that? for example, the generic type T<out D> is covariant on D (hence covariantly valid), hence we can say that the type parameter D is covariantly valid. Am I right?

4) A constructed class, struct, enum, interface or delegate type X might be valid covariantly. To determine if it is, we examine each type argument differently, depending on whether the corresponding type parameter was declared as covariant (out), contravariant (in), or invariant (neither). (Of course the generic type parameters of classes and structs will never be declared 'out' or 'in'; they will always be invariant.) If the ith type parameter was declared as covariant, then Ti must be valid covariantly. If it was declared as contravariant, then Ti must be valid contravariantly. If it was declared as invariant, then Ti must be valid invariantly.

This last rule, from top to bottom, is utterly ambiguous.

Are we talking about a generic type's variance on all of its in/out/invariant type parameters? By definition, A generic type can be covariant/contravariant/invariant on one type paramter at a time. To be covariant or invariant, in this case, on all of it's type parameters at once doesn't hold any meaning. What could that mean?

Moving forward. To determine if the generic type is covariantly valid, we examine its type arguments (not type paramters). So if the corresponding type parameter is covariant/contravariant/invariant, then the type argument is valid covariantly/contravariantly/invariantly respectively ...

I need this rule be explained in more depth.


Edit: Thanks Eric. Greatly appreciated!

I do perfectly understand what valid covariantly/contravariantly/invariantly mean. A type is valid covriantly, if it's definitely not contravariant, which means that it can be invariant. perfectly fine!

For the 4th rule, you follow the procedure of how to determine whether a constructed generic type is valid covariantly, as defined in the rule. But, how do you determine if a type argument that's declared as covariant (out) is covariantly valid?

For example, in the closed constructed interface I { } of the generic interface I { ... }, shouldn't the very fact that the type argument object is declared as a covariant type parameter(out U) in the generic interface declaration mean that the type argument object is covariant? I think it should. Cuz that's the very definition of being covariant.

Also, the second rule:

2) An array type T[] where T is valid covariantly.

What does the array element type T being valid covariantly mean? Do you mean the element type being a value type (invariant in this case) or a reference type (covariant in this case)?

Cuz the projection TT[] is only variant if T is reference type.

like image 743
Garrett Biermann Avatar asked Apr 26 '13 22:04

Garrett Biermann


2 Answers

You are right that the last rule is the hardest one to understand but I assure you it is not ambiguous.

An example or two will help. Consider this type declaration:

interface I<in T, out U, V> { ... }

Is this type covariantly valid?

I<string, object, int> { }

Let's go through our definition.

To determine if it is, we examine each type argument differently, depending on whether the corresponding type parameter was declared as covariant (out), contravariant (in), or invariant (neither).

OK, so the type arguments are string, object and int. The corresponding parameters are in T, out U and V, respectively.

If the ith type parameter was declared as covariant (out), then Ti must be valid covariantly.

The second type parameter is out U, so object must be valid covariantly. It is.

If it was declared as contravariant (in), then Ti must be valid contravariantly.

The first was declared in T, so string must be valid contravariantly. It is.

If it was declared as invariant, then Ti must be valid invariantly.

The third V was invariant, so int must be valid invariantly; it must be both valid contravariantly and covariantly. It is.

We pass all three checks; the type I<string, object, int> is valid covariantly.

OK, that one was easy.

Now let's look at a harder one.

interface IEnumerable<out W> { ... }
interface I<in T, out U, V> 
{
    IEnumerable<T> M();
}

IEnumerable<T> inside I is a type. Is IEnumerable<T> as used inside I valid covariantly?

Let's go through our definition. We have type argument T corresponding to type parameter out W. Note that T is a type parameter of I and a type argument of IEnumerable.

If the ith type parameter (W) was declared as covariant (out), then Ti (T) must be valid covariantly.

OK, so for IEnumerable<T> in I to be valid covariantly, T must be valid covariantly. Is it? NO. T was declared as in T. A type parameter that is declared in is never valid covariantly. Therefore the type IEnumerable<T> as used inside I is not valid covariantly, because the "must" condition is violated.

Again, like I said in my answer to your previous question, if "valid covariantly" and "valid contravariantly" are giving you grief, just give them different names. They are well-defined formal properties; you can call them anything you want if it makes it easier for you to understand.

like image 102
Eric Lippert Avatar answered Oct 06 '22 00:10

Eric Lippert


No, your annotations are messed up.

That article is really hard to understand, in part because "valid covariantly" has nothing at all to do with covariance. Eric does point that out, but it means for every sentence you have to "unthink" the natural meaning, then think in terms of these weird definitions for "valid covariantly", "valid contravariantly", and "valid invariantly".

I strongly recommend you instead read about the Liskov Substitution Principle and then think about substitutability. Covariance, contravariance, and invariance have very simple definitions when looked at from the LSP perspective.

Then, you may notice that the C# rules at compile-time don't exactly match up with LSP (unfortunately -- and this is mainly a mistake made in Java and copied into C# to help court Java programmers). On the other hand, at runtime the LSP rules have to be followed, so if you start with those, you'll write code that both compiles and runs correctly, which I think is a more worthwhile endeavor than learning the C# language rules (unless you're writing a C# compiler).

like image 20
Ben Voigt Avatar answered Oct 06 '22 00:10

Ben Voigt