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];
Siames开发者_运维问答e[] 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 aTBig
- And because
smallOutputter
was assigned withbigOutputter
:- internally,
smallOutputter.getAnInstance()
will returnTBig
- And
TBig
can be converted toTSmall
- the conversion is done and the output is
TSmall
.
- internally,
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 returnTSmall
- And because
bigOutputter
was assigned withsmallOutputter
:- internally,
bigOutputter.getAnInstance()
will returnTSmall
- But
TSmall
cannot be converted toTBig
!! - This then is not possible.
- internally,
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 insmallInstance
smallAnalyser.isInstanceCool(bigInstance)
can also use the methods (it's looking only at theTSmall
part ofTBig
)
- And since
bigAnalyser
was assigned withsmallAnalyer
:- it's totally ok to call
bigAnalyser.isInstanceCool(bigInstance)
- it's totally ok to call
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 inbigInstance
- but
bigAnalyser.isInstanceCool(smallInstance)
cannot findTBig
methods inTSmall
!!! And it's not guaranteed that thissmallInstance
is even aTBig
converted.
- And since
smallAnalyser
was assigned withbigAnalyser
:- calling
smallAnalyser.isInstanceCool(smallInstance)
will try to findTBig
methods in the instance - and it may not find the
TBig
methods, because thissmallInstance
may not be aTBig
instance.
- calling
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 outputT
- Another interface using
out
having only methods that don't takeT
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:
精彩评论