相同类型参数的协方差和逆变

C#规范声明参数类型不能同时具有协变性和逆变性。

这在创建协变或逆变接口时很明显,您可以分别使用“out”或“in”来修饰类型参数。 没有选项可以同时允许两者(“outin”)。

这种限制只是一种语言特定的约束,还是存在更深层次,更基本的理由,这种理由会使你不希望你的类型既是协变的又是逆变的?

编辑:

我的理解是arrays实际上既是协变的又是逆变的。

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; 

正如其他人所说的那样,generics类型在协变和逆变方面在逻辑上是不一致的。 到目前为止,这里有一些很好的答案,但是我再添加两个。

首先,阅读关于方差“有效性”主题的文章:

http://blogs.msdn.com/b/ericlippert/archive/2009/12/03/exact-rules-for-variance-validity.aspx

根据定义,如果类型是“协变有效”,则它不能以逆变方式使用 。 如果它是“违反有效的”那么它就不能以协变的方式使用兼容有效和违反有效的东西不能以协变或逆变的方式使用 。 也就是说,它是不变的 。 因此,有协变性和逆变性的结合:它们的结合是不变的

其次,让我们假设你有了你的愿望,并且有一种类型注释按我认为你想要的方式工作:

 interface IBurger {} 

假设你有一个IBurger 。 因为它是协变的,所以可以转换为IBurger 。 因为它是逆变的,所以它又可以转换为IBurger ,即使“string”和“Exception”没有任何共同之处。 基本上“进出”意味着对于任何两种参考类型T1和T2, IBurger可转换为任何类型的IBurger这有用吗? 你会用这样的function什么? 假设你有一个IBurger ,但该对象实际上是一个IBurger 。 你能做些什么,既利用了type参数是Exception的事实,又允许那个类型参数成为一个完整的谎言,因为“真正的”类型参数是一个完全不相关的类型?

回答你的后续问题:涉及数组的隐式引用类型转换是协变的 ; 它们不是逆变的。 你能解释为什么你错误地认为它们是逆变的吗?

协方差和逆变是相互排斥的。 你的问题就像询问集合A是否可以是集合B的超集和集合B的子集。为了使集合A既是集合B的子集又是超集集合,集合A必须等于集合B,那么你只想询问集合A是否等于集合B.

换句话说,在同一个论点上要求协方差和逆变就像要求没有方差(不变性),这是默认的。 因此,不需要关键字来指定它。

对于您从未输入的类型,协方差是可能的(例如,成员函数可以将其用作返回类型或out参数,但从不作为输入参数使用)。 对于从未输出的类型(例如,作为输入参数,但从不作为返回类型或out参数),可能存在逆变化。

如果您创建了一个covariant和contravariant类型参数,则无法输入它而无法输出它 – 您根本无法使用它。

没有out和in keywords参数是Covariance和Contravariance不是吗?

in表示参数只能用作函数参数类型

out表示参数只能用作返回值类型

没有输入和输出意味着它可以用作参数类型和返回值类型

这种限制只是一种语言特定的约束,还是存在更深层次,更基本的理由,这种理由会使你不希望你的类型既是协变的又是逆变的?

不,有一个更简单的理由基于基本逻辑(或者只是常识,无论你喜欢哪种):一个陈述不能同时是真的而不是真的。

协方差表示S <: T ⇒ G <: G并且逆变量表示S <: T ⇒ G <: G 。 应该很明显,这些在同一时间永远不会成立。

你可以用“Covariant”做什么?

Covariant使用修饰符out ,这意味着类型可以是方法的输出,但不是输入参数。

假设你有这些类和接口:

 interface ICanOutput { T getAnInstance(); } class Outputter : ICanOutput { public T getAnInstance() { return someTInstance; } } 

现在假设您有类型TBig inheiriting TSmall 。 这意味着TBig实例也始终是TSmall实例; 但是TSmall实例并不总是TBig实例。 (这些名称被选择为易于在TSmall内部显示TBig

当你这样做时(经典的变体分配):

 //a real instance that outputs TBig Outputter bigOutputter = new Outputter(); //just a view of bigOutputter ICanOutput smallOutputter = bigOutputter; 
  • bigOutputter.getAnInstance()将返回一个TBig
  • 并且因为smallOutputter被分配了bigOutputter
    • 在内部, smallOutputter.getAnInstance()将返回TBig
    • TBig可以转换为TSmall
    • 转换完成,输出为TSmall

如果是相反的(好像它是反对变体):

 //a real instance that outputs TSmall Outputter smallOutputter = new Outputter(); //just a view of smallOutputter ICanOutput bigOutputter = smallOutputter; 
  • smallOutputter.getAnInstance()将返回TSmall
  • 并且因为bigOutputter被分配了smallOutputter
    • 在内部, bigOutputter.getAnInstance()将返回TSmall
    • 但是TSmall无法转换为TBig !!
    • 这是不可能的。

这就是为什么contra variant”类型不能用作输出类型的原因


你可以用“Contravariant”做什么?

遵循上述相同的想法,逆变使用修饰符,意味着类型可以是方法的输入参数,但不是输出参数。

假设你有这些类和接口:

 interface ICanInput { bool isInstanceCool(T instance); } class Analyser : ICanInput { bool isInstanceCool(T instance) { return instance.amICool(); } } 

再次,假设TBiginheritanceTSmall类型。 这意味着TBig可以完成TBig所做的一切(它拥有所有TSmall成员和更多)。 但是TSmall不能做TBig所做的一切( TBig有更多的成员)。

当你这样做时(一个经典的反对派变种):

 //a real instance that can use TSmall methods Analyser smallAnalyser = new Analyser(); //this means that TSmall implements amICool //just a view of smallAnalyser ICanInput bigAnalyser = smallAnalyser; 
  • smallAnalyser.isInstanceCool
    • smallAnalyser.isInstanceCool(smallInstance)可以使用smallInstance的方法
    • smallAnalyser.isInstanceCool(bigInstance)也可以使用这些方法(它只查看TSmall部分)
  • 因为bigAnalyser被分配了smallAnalyer
    • 调用bigAnalyser.isInstanceCool(bigInstance)是完全可以的

如果是相反的(就好像它是变体):

 //a real instance that can use TBig methods Analyser bigAnalyser = new Analyser(); //this means that TBig has amICool, but not necessarily that TSmall has it //just a view of bigAnalyser ICanInput smallAnalyser = bigAnalyser; 
  • 对于bigAnalyser.isInstanceCool
    • bigAnalyser.isInstanceCool(bigInstance)可以使用bigInstance的方法
    • 但是bigAnalyser.isInstanceCool(smallInstance)TSmall找不到TBig方法! 并且不保证这个smallInstance甚至TBig转换为TBig
  • 因为smallAnalyser被分配了bigAnalyser
    • 调用smallAnalyser.isInstanceCool(smallInstance)将尝试在实例中查找TBig方法
    • 并且它可能找不到TBig方法,因为这个smallInstance可能不是TBig实例。

这就是co variant”类型不能用作输入参数的原因


加入两者

现在,当你将两个“无法”加在一起时会发生什么?

  • 不能这个+不能那个=什么都不能

你能做什么?

我没有测试过这个(但是……我在想我是否有理由这样做),但似乎没问题,只要你知道你会有一些限制。

如果您只清楚地分离仅输出所需类型的方法和仅将其作为输入参数的方法,则可以使用两个接口实现类。

  • 一个接口使用in并且只有不输出T
  • 使用的另一个接口只有不使用T作为输入的方法

在所需的情况下使用每个接口,但不要尝试将一个接口分配给另一个接口。