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;
Generic type parameters cannot be both covariant and contravariant.
Why? This has to do with the restrictions which
in
andout
modifiers impose. If we wanted to make our generic type parameter both covariant and contravariant, we would basically say:Which would essentially make our generic interface non-generic.
I explained it in detail under another question:
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 orout
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.
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.
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 meansS <: T ⇒ G<T> <: G<S>
. It should be pretty obvious that these can never be true at the same time.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
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:
Now suppose you have the types
TBig
inheiritingTSmall
. This means that aTBig
instance is always aTSmall
instance too; but aTSmall
instance is not always aTBig
instance. (The names were chosen to be easy to visualizeTSmall
fitting insideTBig
)When you do this (a classic covariant assignment):
bigOutputter.getAnInstance()
will return aTBig
smallOutputter
was assigned withbigOutputter
:smallOutputter.getAnInstance()
will returnTBig
TBig
can be converted toTSmall
TSmall
.If it was the contrary (as if it were contravariant):
smallOutputter.getAnInstance()
will returnTSmall
bigOutputter
was assigned withsmallOutputter
:bigOutputter.getAnInstance()
will returnTSmall
TSmall
cannot be converted toTBig
!!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:
Again, suppose the types
TBig
inheritingTSmall
. This means thatTBig
can do everything thatTSmall
does (it has allTSmall
members and more). ButTSmall
cannot do everythingTBig
does (TBig
has more members).When you do this (a classic contravariant assignment):
smallAnalyser.isInstanceCool
:smallAnalyser.isInstanceCool(smallInstance)
can use the methods insmallInstance
smallAnalyser.isInstanceCool(bigInstance)
can also use the methods (it's looking only at theTSmall
part ofTBig
)bigAnalyser
was assigned withsmallAnalyer
:bigAnalyser.isInstanceCool(bigInstance)
If it was the contrary (as if it were covariant):
bigAnalyser.isInstanceCool
:bigAnalyser.isInstanceCool(bigInstance)
can use the methods inbigInstance
bigAnalyser.isInstanceCool(smallInstance)
cannot findTBig
methods inTSmall
!!! And it's not guaranteed that thissmallInstance
is even aTBig
converted.smallAnalyser
was assigned withbigAnalyser
:smallAnalyser.isInstanceCool(smallInstance)
will try to findTBig
methods in the instanceTBig
methods, because thissmallInstance
may not be aTBig
instance.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 outputT
out
having only methods that don't takeT
as inputUse each interface at the required situation, but don't try to assign one to another.