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 int
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.
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
smallOutputter
was assigned with bigOutputter
:
smallOutputter.getAnInstance()
will return TBig
TBig
can be converted to TSmall
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
bigOutputter
was assigned with smallOutputter
:
bigOutputter.getAnInstance()
will return TSmall
TSmall
cannot be converted to TBig
!!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
)bigAnalyser
was assigned with smallAnalyer
:
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;
bigAnalyser.isInstanceCool
:
bigAnalyser.isInstanceCool(bigInstance)
can use the methods in bigInstance
bigAnalyser.isInstanceCool(smallInstance)
cannot find TBig
methods in TSmall
!!! And it's not guaranteed that this smallInstance
is even a TBig
converted. smallAnalyser
was assigned with bigAnalyser
:
smallAnalyser.isInstanceCool(smallInstance)
will try to find TBig
methods in the instance 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?
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.
in
and having only methods that don't output T
out
having only methods that don't take T
as inputUse each interface at the required situation, but don't try to assign one to another.
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 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.