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.