Covariance and Contravariance on the same type arg

2019-02-09 06:08发布

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; 

7条回答
祖国的老花朵
2楼-- · 2019-02-09 06:21

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:

查看更多
趁早两清
3楼-- · 2019-02-09 06:33

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.

查看更多
何必那么认真
4楼-- · 2019-02-09 06:38

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.

查看更多
家丑人穷心不美
5楼-- · 2019-02-09 06:38

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.

查看更多
你好瞎i
6楼-- · 2019-02-09 06:44

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

查看更多
Animai°情兽
7楼-- · 2019-02-09 06:44

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.

查看更多
登录 后发表回答