Variance rules in C# - 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 T → T[] is only variant if T is reference type.

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.

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

how do you determine if a type argument that's declared as covariant (out) is covariantly valid?
Read rule 3.
in the closed constructed interface I{string, object int> of the generic interface I<in T, out U, V>, 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?
First, you're using "covariant" where you mean "covariantly valid". Remember, these are different things.
Second, let's go through it again. Is object covariantly valid? Yes, by rule 1. Is I<string, object, int> covariantly valid? Yes, by rule 3, which states that:
The type argument corresponding to T must be contravariantly valid.
The type argument corresponding to U must be covariantly valid.
The type argument corresponding to V must be both.
Since all three conditions are met, I<string, object, int> is covariantly valid.
In "An array type T[] where T is valid covariantly" what does the array element type T being valid covariantly mean?
I don't understand the question. We're defining what "covariantly valid" means. Rule 2 is part of the definition of "covariantly valid".
As an example, is object[] covariantly valid? Yes, because object is covariantly valid. If we had:
interface IFoo<out T> { T[] M(); }
Is T[] covariantly valid? Yes, because T is covariantly valid.
If we had
interface IBar<in T> { T[] M(); }
Is T[] covariantly valid? No. For an array type to be covariantly valid its element type must be covariantly valid, but T is not.

Related

Why won't a Func with a nullable return type fit into a Dictionary holding Funcs with object return types?

I am attempting to build a Dictionary that looks like this:
var getValueFuncs = new Dictionary<TypeID, Func<string, object>>
{
{TypeID.BINARY, NotMine.GetBinaryValue},
{TypeID.BOOLEAN, NotMine.GetBooleanValue},
{TypeID.DATE, NotMine.GetDateTimeValue},
{TypeID.DOUBLE, NotMine.GetFloat64Value},
{TypeID.LONG, NotMine.GetInteger32Value},
{TypeID.STRING, NotMine.GetStringValue}
};
The various Funcs that I want to put into it all have different return types.
public interface INotMine : ICollection, ICloneable
{
byte[] GetBinaryValue(string propName);
bool? GetBooleanValue(string propName);
DateTime? GetDateTimeValue(string propName);
double? GetFloat64Value(string propName);
int? GetInteger32Value(string propName);
string GetStringValue(string propName);
}
Note that NotMine is a separate class, that it inherits from INotMine, and that I cannot change it.
The two lines to initialize the Dictionary with the procedures that return byte[] and string compile with no trouble. The four lines for procedures that return nullable data types (i.e. bool?, DateTime?, double?, and int?) will not compile. They each report an error like this one:
bool? INotMine.GetBooleanValue(string propName)
'bool? INotMine.GetBooleanValue(string)' has the wrong return type
Expected a method with 'object GetBooleanValue(string)' signature
Given that everything inherits from object, why will those lines not compile and go into my Dictionary nicely?
EDIT:
Before he deleted it, someone posted an answer that contained an elegant work-around that looked like this:
{TypeID.BINARY, s => NotMine.GetBinaryValue(s)}
I'm grateful for that, and I'll use that, but I'd still like to know why the original lines won't compile.
Consider the following:
Func<Animal, Turtle> fat = whatever;
Func<Mammal, Reptile> fmr = fat;
This is legal. fat can take any Animal, and the caller of fmr promises to pass a Mammal, which is always an Animal. Similarly fmr requires that the function return a Reptile, and fat always returns a Turtle, which is a Reptile.
This kind of conversion is called a variant conversion. Specifically, the "I require any Animal so I'll accept Mammal" is called a contravariant conversion, and the "I return a Turtle so I meet your need for a Reptile" is a covariant conversion.
C# supports covariant and contravariant conversions on generic types only under these circumstances:
The type in question is a delegate or interface.
The type has been marked as safe for variance, and the compiler has confirmed that it is safe.
The type arguments that are varying are all reference types.
You meet the first two conditions -- your Func<string, object> is a delegate type known to be safe to be contravariant in the argument and covariant in the return. However you do not meet the third condition. bool? DateTime?, double? and int? are not reference types. Therefore, variant conversion rules do not apply, and the conversions are illegal.
That would cover converting from Func<string, bool?> to Func<string, object>. But that's not what you're doing; you are converting from the method group containing a function that returns bool?. Does that matter?
No. The rules are the same for method group conversions. A covariant conversion from a method group containing a method that returns bool? to a Func<string, object> is not legal because bool? is still required to be a reference type to make that conversion legal.
No matter how you slice it, you are stuck. You cannot convert a method or delegate that returns bool? directly to a delegate that returns object.
The reason for this is summed up nicely in Jeroen's comment on the question. Where does the boxing conversion go? A boxing conversion is code that allocates heap memory and therefore that code has to go somewhere, but there is nowhere for the compiler to put it and still maintain reference identity on the delegate.
As you correctly note, if you provide your own lambda then the compiler has a place to put the boxing conversion; it sticks it on top of the return from the lambda. The price you pay for this is that the lambda is now a delegate that refers to the lambda body, and not directly to GetBooleanValue, so there is a tiny indirection penalty there.

Covariance and Contravariance with Func in generics

I need more information about variance in generics and delegates. The following code snippet does not compile:
Error CS1961 Invalid variance: The type parameter 'TIn' must be
covariantly valid on 'Test.F(Func)'. 'TIn' is
contravariant.
public interface Test<in TIn, out TOut>
{
TOut F (Func<TIn, TOut> transform);
}
The .net Func definition is as follows:
public delegate TResult Func<in T, out TResult> (T arg);
Why the compiler complains about TIn being contravariant and TOut - covariant while the Func expects exactly the same variance?
EDIT
The main constraint for me is that I want my Test interface to have TOut as covariant in order to use it something like this:
public Test<SomeClass, ISomeInterface> GetSomething ()
{
return new TestClass<SomeClass, AnotherClass> ();
}
Given that public class AnotherClass : ISomeInterface.
I need more information about variance in generics and delegates.
I wrote an extensive series of blog articles on this feature. Though some of it is out of date -- since it was written before the design was finalized -- there's lots of good information there. In particular if you need a formal definition of what variance validity is, you should carefully read this:
https://blogs.msdn.microsoft.com/ericlippert/2009/12/03/exact-rules-for-variance-validity/
See my other articles on my MSDN and WordPress blogs for related topics.
Why the compiler complains about TIn being contravariant and TOut - covariant while the Func expects exactly the same variance?
Let's slightly rewrite your code and see:
public delegate R F<in T, out R> (T arg);
public interface I<in A, out B>{
B M(F<A, B> f);
}
The compiler must prove that this is safe, but it is not.
We can illustrate that it is not safe by supposing that it is, and then discovering how it can be abused.
Let's suppose we have an Animal hierarchy with the obvious relationships, eg, Mammal is an Animal, Giraffe is a Mammal, and so on. And let's suppose that your variance annotations are legal. We should be able to say:
class C : I<Mammal, Mammal>
{
public Mammal M(F<Mammal, Mammal> f) {
return f(new Giraffe());
}
}
I hope you agree this is a perfectly valid implementation. Now we can do this:
I<Tiger, Animal> i = new C();
C implements I<Mammal, Mammal>, and we've said that the first one can get more specific, and the second can get more general, so we've done that.
Now we can do this:
Func<Tiger, Animal> f = (Tiger t) => new Lizard();
That's a perfectly legal lambda for this delegate, and it matches the signature of:
i.M(f);
And what happens? C.M is expecting a function that takes a giraffe and returns a mammal, but it's been given a function that takes a tiger and returns a lizard, so someone is going to have a very bad day.
Plainly this must not be allowed to happen, but every step along the way was legal. We must conclude that the variance itself was not provably safe, and indeed, it was not. The compiler is right to reject this.
Getting variance right takes more than simply matching the in and out annotations. You've got to do so in a manner that does not allow this sort of defect to exist.
That explains why this is illegal. To explain how it is illegal, the compiler must check that the following is true of B M(F<A, B> f);:
B is valid covariantly. Since it is declared "out", it is.
F<A, B> is valid contravariantly. It is not. The relevant portion of the definition of "valid contravariantly" for a generic delegate is: If the ith type parameter was declared as contravariant, then Ti must be valid covariantly. OK. The first type parameter, T, was declared as contravariant. Therefore the first type argument A must be valid covariantly. But it is not valid covariantly, because it was declared contravariant. And that's the error you're getting. Similarly, B is also bad because it must be valid contravariantly, but B is covariant. The compiler does not go on to find additional errors after it finds the first problem here; I considered it but rejected it as being a too-complex error message.
I note also that you would still have this problem even if the delegate were not variant; nowhere in my counterexample did we use the fact that F is variant in its type parameters. A similar error would be reported if we tried
public delegate R F<T, R> (T arg);
instead.
Variance is about being able to replace type parameters with either more or less derived types than originally declared. For example, IEnumerable<T> is covariant for T, meaning if you start with a reference to a IEnumerable<U> object, you can assign that reference to a variable having type IEnumerable<V>, where V is assignable from U (e.g. U inherits V). This works, because any code trying to use the IEnumerable<V> wants to receive only values of V, and since V is assignable from U, receiving only values of U is also valid.
For covariant parameters like T, you have to assign to a type where the destination type is the same as T, or assignable from T. For contravariant parameters, it has to go the other way. The destination type has to be the same as, or assignable to, the type parameter.
So, how does the code you are trying to write work in that respect?
When you declare Test<in TIn, out TOut>, you are promising that it will be valid to assign an instance of that interface Test<TIn, TOut> to any destination having the type Test<U, V> where U can be assigned to TIn and TOut can be assigned to V (or they are identical, of course).
At the same time, let's consider what your transform delegate is to expect. The Func<T, TResult> type variance requires that if you want to assign that value to something else, it also meets the variance rules. That is, a destination Func<U, V> must have U assignable from T, and TResult assignable from V. This ensures that your delegate target method which is expecting to receive a value of U will get one of those, and the value returned by the method, having type V, can be accepted by the code receiving it.
Importantly, your interface method F() is the one doing the receiving! The interface declaration promises that TOut will be used only as output from the interface members. But through the use of the transform delegate, the method F() will receive a value of TOut, making that input to the method. Likewise, the method F() is allowed to pass a value of TIn to the transform delegate, making that an output of your interface implementation, even though you've promised that TIn is used only as input.
In other words, every layer of call reverses the sense of the variance. Members in the interface have to use covariant type parameters as output only and contravariant parameters as input only. But those parameters become reversed in sense when they are used in delegate types passed to or returned from interface members, and have to comply with the variance in that respect.
A concrete example:
Suppose we have an implementation of your interface, Test<object, string>. If the compiler were to allow your declaration, you'd be permitted to assign a value of that implementation Test<object, string> to a variable having the type Test<string, object>. That is, the original implementation promises to allow as input any thing having type object and return only values having the type string. It's safe for code declared as Test<string, object> to work with this, because it will pass string objects to an implementation that requires objects values (string is an object), and it will receive values having the type object from an implementation that returns string values (again, string is an object, so also safe).
But your interface implementation expects code to pass a delegate of type Func<object, string>. If you were allowed to treat (as above) your interface implementation as a Test<string, object> instead, then the code using your re-cast implementation would be able to pass a delegate of Func<string, object> to the method F(). The method F() in the implementation is allowed to pass any value of type object to the delegate, but that delegate, being of type Func<string, object>, is expecting only values having the type string to be passed to it. If F() passes something else, e.g. just a plain old new object(), the delegate instance won't be able to use it. It's expecting a string!
So, in fact, the compiler is doing exactly what it's supposed to: it's preventing you from writing code that is not type-safe. As declared, if you were permitted to use that interface in a variant way, you would in fact be able to write code that while allowed at compile-time, could break at run-time. Which is the exact opposite of the whole point of generics: to be able to determine at compile-time that the code is type-safe!
Now, how to solve the dilemma. Unfortunately, there's not enough context in your question to know what the right approach is. It's possible that you simply need to give up on variance. Often, there's not actually any need to make types variant; it's a convenience in some cases, but not required. If that's the case, then just don't make the interface's parameters variant.
Alternatively, it's possible you really do want the variance and thought it would be safe to use the interface in a variant way. That's harder to solve, because your fundamental assumption was just incorrect and you will need to implement the code some other way. The code would compile if you could reverse the parameters in the Func<T, TResult>. I.e. make the method F(Func<TOut, TIn> transform). But there's not anything in your question that suggests that's actually possible in your scenario.
Again, without more context it's impossible to say what "other way" would work for you. But, hopefully now that you understand the hazard in the code the way you've written it now, you can revisit the design decision that led you to this not-type-safe interface declaration, and can come up with something that works. If you have trouble with that, post a new question that provides more detail as to why you thought this would be safe, how you're going to use the interface, what alternatives you've considered, and why none of those work for you.
TIn = the class knows how to read it, and the implementation is allowed to treat it as a type that is less derived than it actually is. You might pass it an instance that is more derived than expected, but that doesn't matter, because the derived class can do everything that the base class can do.
TOut = the implementation knows to to produce one, and the implementation is allowed to produce a type that is more derived than the caller is expecting. Again, it doesn't matter-- the caller can assign a more derived class to a less derived variable with no problem.
But--
If you pass the class a Func<TIn, TOut>, and you expect the class to be able to call it, then the class will have to be able to produce a TIn and read the TOut. Which is the opposite of the above.
Why can't it? Well, I already mentioned that the class can treat TIn as something that is less derived. If it attempts to call the function with an argument that is less derived, it won't work (what if the function is expecting to be able to call string.Length but the class passes it an object?). Also, if it attempts to read the results of the function as something that is more derived, that will fail as well.
You can eliminate the problem by eliminating the variance-- get rid of the in and out keywords-- which will render the class unable to substitute less/more derived types (this is called "invariance") but will allow you to both read and write the types.
Remove in and out -keywords from the interface definition:
public interface Test<TIn, TOut>{
TOut F (Func<TIn, TOut> transform);
}
remove the in and out key words:
public interface Test<TIn, TOut>
{
TOut F (Func<TIn, TOut> transform);
}
you can read about the meaning of them here:
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/in-generic-modifier
A type can be declared contravariant in a generic interface or delegate if it is used only as a type of method arguments and not used as a method return type
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/out-generic-modifier
The type parameter is used only as a return type of interface methods and not used as a type of method arguments.

Variance when creating interfaces

public interface SomeInterfaceName<out T> where T : struct
{
T? SomePropertyName { get; }
}
The error I get is:
error CS1961: Invalid variance: The type parameter 'T' must be invariantly valid on 'SomeInterfaceName.SomePropertyName'. 'T' is covariant.
I do not need this to be Covariant. I could as well delete the out keyword. But then Resharper is suggesting I could use Covariance and I agree, I don't see why I could not. I'm only using T as return value. Or is it because Nullable<> does not support it?
Can anybody explain the error?
It's pointless to make a type parameter with a struct constraint covariant. Generic variance isn't supported for value type type arguments at all - so for example, there's no conversion from IEnumerable<int> to either IEnumerable<long> or IEnumerable<object> even though there are conversions from int to both long and object.
The problem you're actually running into is that Nullable<T> isn't covariant, but you're trying to use T in a property of type Nullable<T>. That's what's causing the error.
Personally I think it would be better if type parameters with struct constraints couldn't be declared to be covariant or contravariant (given that it won't be useful) but that in itself isn't prohibited.

What does "out" mean before a Generic type parameter?

I've just saw an unfamiliar syntax while looking for GroupBy return type:
public interface IGrouping<out TKey, out TElement> : IEnumerable<TElement>
MSDN Source
I know what does out mean in methods, but not in a generics interface.
What does out mean in a generic type?
It denotes a covariant parameter. See also the description on MSDN. Essentially it says, that IGrouping<Aderived, Bderived> can be regarded as IGrouping<Abase, Bbase>, hence you can
IGrouping<Aderived, Bderived> gr = MakeGrouping(...);
IGrouping<Abase, Bbase> grBase = gr;
if Aderived is an interface or a type derived from Abase. This is a feature that comes in handy when you want to call a method that requires a parameter of type IGrouping<Abase, Bbase>, but you only got an object of type IGrouping<Aderived, Bderived>. In this case, both types can be considered equivalent due to the covariance of their type parameters.
It is one of the two generic modifiers introduces in C# 4.0 (Visual Studio 2010).
It signifies that the generic parameter it is declared on is covariant.
The in modifier signifies the generic parameter it is declared on is contravariant.
See out (Generic Modifier) and in (Generic Modifier) on MSDN.
out just means that the type is only used for output e.g.
public interface Foo<out T>
{
T Bar()
}
There also is a modifier in that means that the type is only used for input e.g.
public interface Foo<in T>
{
int Bar(T x)
}
These are used because Interfaces with in are covariant in T and Interfaces with out are contravariant in T.
out keyword in this context would indicate the corresponding type parameter to be covariant simply speaking - covariance enables you to use a more derived type than that specified by the generic parameter.
BTW, see this ten part series from Eric Lippert to understand more about covariance and contra-variance: http://blogs.msdn.com/b/ericlippert/archive/2007/10/16/covariance-and-contravariance-in-c-part-one.aspx

Covariance and Contravariance on the same type argument

The C# spec states that an argument type cannot be both covariant and contravariant at the same time.
This is apparent when creating a covariant or contravariant interface you decorate your type parameters with "out" or "in" respectively. There is not option that allows both at the same time ("outin").
Is this limitation simply a language specific constraint or are there deeper, more fundamental reasons based in category theory that would make you not want your type to be both covariant and contravariant?
Edit:
My understanding was that arrays were actually both covariant and contravariant.
public class Pet{}
public class Cat : Pet{}
public class Siamese : Cat{}
Cat[] cats = new Cat[10];
Pet[] pets = new Pet[10];
Siamese[] siameseCats = new Siamese[10];
//Cat array is covariant
pets = cats;
//Cat array is also contravariant since it accepts conversions from wider types
cats = siameseCats;
As others have said, it is logically inconsistent for a generic type to be both covariant and contravariant. There are some excellent answers here so far, but let me add two more.
First off, read my article on the subject of variance "validity":
http://blogs.msdn.com/b/ericlippert/archive/2009/12/03/exact-rules-for-variance-validity.aspx
By definition, if a type is "covariantly valid" then it is not usable in a contravariant way. If it is "contravariantly valid" then it is not usable in a covariant way. Something that is both covariantly valid and contravariantly valid is not usable in either a covariant or contravariant way. That is, it is invariant. So, there is the union of covariant and contravariant: their union is invariant.
Second, let's suppose for a moment that you got your wish and that there was a type annotation that worked the way I think you want:
interface IBurger<in and out T> {}
Suppose you have an IBurger<string>. Because it is covariant, that is convertible to IBurger<object>. Because it is contravariant, that is in turn convertible to IBurger<Exception>, even though "string" and "Exception" have nothing whatsoever in common. Basically "in and out" means that IBurger<T1> is convertible to any type IBurger<T2> for any two reference types T1 and T2. How is that useful? What would you do with such a feature? Suppose you have an IBurger<Exception>, but the object is actually an IBurger<string>. What could you do with that, that both takes advantage of the fact that the type argument is Exception, and allows that type argument to be a complete lie, because the "real" type argument is an utterly unrelated type?
To answer your follow-up question: implicit reference type conversions involving arrays are covariant; they are not contravariant. Can you explain why you incorrectly believe them to be contravariant?
Covariance and contravariance are mutually exclusive. Your question is like asking if set A can be both a superset of set B and a subset of set B. In order for set A to be both a subset and superset of set B, set A must be equal to set B, so then you would just ask if set A is equal to set B.
In other words, asking for covariance and contravariance on the same argument is like asking for no variance at all (invariance), which is the default. Thus, there's no need for a keyword to specify it.
Covariance is possible for types you never input (e.g. member functions can use it as a return type or out parameter, but never as an input parameter). Contravariance is possible for types you never output (e.g. as an input parameter, but never as a return type or out parameter).
If you made a type parameter both covariant and contravariant, you couldn't input it and you couldn't output it -- you couldn't use it at all.
Without out and in keywords argument is Covariance and Contravariance isn't it?
in means that argument can only be used as function argument type
out means that argument can be used only as return value type
without in and out means that it can be used as argument type and as return value type
Is this limitation simply a language specific constraint or are there deeper, more fundamental reasons based in category theory that would make you not want your type to be both covariant and contravariant?
No, there is a much simpler reason based in basic logic (or just common sense, whichever you prefer): a statement cannot be both true and not true at the same time.
Covariance means S <: T ⇒ G<S> <: G<T> and contravariance means S <: T ⇒ G<T> <: G<S>. It should be pretty obvious that these can never be true at the same time.
What you can do with "Covariant"?
Covariant uses the modifier out, meaning that the type can be an output of a method, but not an input parameter.
Suppose you have these class and interface:
interface ICanOutput<out T> { T getAnInstance(); }
class Outputter<T> : ICanOutput<T>
{
public T getAnInstance() { return someTInstance; }
}
Now suppose you have the types TBig inheiriting TSmall. This means that a TBig instance is always a TSmall instance too; but a TSmall instance is not always a TBig instance. (The names were chosen to be easy to visualize TSmall fitting inside TBig)
When you do this (a classic covariant assignment):
//a real instance that outputs TBig
Outputter<TBig> bigOutputter = new Outputter<TBig>();
//just a view of bigOutputter
ICanOutput<TSmall> smallOutputter = bigOutputter;
bigOutputter.getAnInstance() will return a TBig
And because smallOutputter was assigned with bigOutputter:
internally, smallOutputter.getAnInstance() will return TBig
And TBig can be converted to TSmall
the conversion is done and the output is TSmall.
If it was the contrary (as if it were contravariant):
//a real instance that outputs TSmall
Outputter<TSmall> smallOutputter = new Outputter<TSmall>();
//just a view of smallOutputter
ICanOutput<TBig> bigOutputter = smallOutputter;
smallOutputter.getAnInstance() will return TSmall
And because bigOutputter was assigned with smallOutputter:
internally, bigOutputter.getAnInstance() will return TSmall
But TSmall cannot be converted to TBig!!
This then is not possible.
This is why "contravariant" types cannot be used as output types
What you can do with "Contravariant"?
Following the same idea above, contravariant uses the modifier in, meaning that the type can be an input parameter of a method, but not an output parameter.
Suppose you have these class and interface:
interface ICanInput<in T> { bool isInstanceCool(T instance); }
class Analyser<T> : ICanInput<T>
{
bool isInstanceCool(T instance) { return instance.amICool(); }
}
Again, suppose the types TBig inheriting TSmall. This means that TBig can do everything that TSmall does (it has all TSmall members and more). But TSmall cannot do everything TBig does (TBig has more members).
When you do this (a classic contravariant assignment):
//a real instance that can use TSmall methods
Analyser<TSmall> smallAnalyser = new Analyser<TSmall>();
//this means that TSmall implements amICool
//just a view of smallAnalyser
ICanInput<TBig> bigAnalyser = smallAnalyser;
smallAnalyser.isInstanceCool:
smallAnalyser.isInstanceCool(smallInstance) can use the methods in smallInstance
smallAnalyser.isInstanceCool(bigInstance) can also use the methods (it's looking only at the TSmall part of TBig)
And since bigAnalyser was assigned with smallAnalyer:
it's totally ok to call bigAnalyser.isInstanceCool(bigInstance)
If it was the contrary (as if it were covariant):
//a real instance that can use TBig methods
Analyser<TBig> bigAnalyser = new Analyser<TBig>();
//this means that TBig has amICool, but not necessarily that TSmall has it
//just a view of bigAnalyser
ICanInput<TSmall> smallAnalyser = bigAnalyser;
For bigAnalyser.isInstanceCool:
bigAnalyser.isInstanceCool(bigInstance) can use the methods in bigInstance
but bigAnalyser.isInstanceCool(smallInstance) cannot find TBig methods in TSmall!!! And it's not guaranteed that this smallInstance is even a TBig converted.
And since smallAnalyser was assigned with bigAnalyser:
calling smallAnalyser.isInstanceCool(smallInstance) will try to find TBig methods in the instance
and it may not find the TBig methods, because this smallInstance may not be a TBig instance.
This is why "covariant" types cannot be used as input parameters
Joining both
Now, what happens when you add two "cannots" together?
Cannot this + cannot that = cannot anything
What could you do?
I haven't tested this (yet... I'm thinking if I'll have a reason to do this), but it seems to be ok, provided you know you will have some limitations.
If you have a clear separation of the methods that only output the desired type and methods that only take it as an input parameter, you can implement your class with two interfaces.
One interface using in and having only methods that don't output T
Another interface using out having only methods that don't take T as input
Use each interface at the required situation, but don't try to assign one to another.
Generic type parameters cannot be both covariant and contravariant.
Why? This has to do with the restrictions which in and out modifiers impose. If we wanted to make our generic type parameter both covariant and contravariant, we would basically say:
None of the methods of our interface returns T
None of the methods of our interface accepts T
Which would essentially make our generic interface non-generic.
I explained it in detail under another question:

Categories