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

时间:2010-12-24 20:22:52

标签: c# covariance contravariance variance

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

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

这种限制是否只是一种语言特定的约束,还是基于类别理论的更深层次,更根本的原因会让你不希望你的类型既是协变的又是逆变的?

修改

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

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 个答案:

答案 0 :(得分:24)

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

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

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

根据定义,如果某个类型是“covariantly valid”,则它不能以逆向方式使用。如果它是“违反有效的”,那么无法以协变的方式使用两者的共同有效和违反有效的东西无法以协变或逆变的方式使用。也就是说,它是不变的。所以,协变和逆变的联合:它们的联合是不变的

其次,我们假设您已经实现了您的愿望,并且有一种类型注释按照我认为您想要的方式工作:

interface IBurger<in and out T> {}

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

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

答案 1 :(得分:8)

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

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

答案 2 :(得分:5)

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

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

答案 3 :(得分:1)

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

中的

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

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

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

答案 4 :(得分:0)

  

这种限制是否只是一种语言特定的约束,还是基于类别理论的更深层次,更根本的原因会让你不希望你的类型既是协变的又是逆变的?

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

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

答案 5 :(得分:0)

您可以使用&#34; Covariant&#34;

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

假设您有这些类和接口:

interface ICanOutput<out T> { T getAnInstance(); }

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

现在假设您有类型TBig inheiriting TSmall。这意味着TBig实例也始终是TSmall实例;但是TSmall实例并不总是TBig实例。 (选择的名称很容易可视化TSmall拟合TBig}

执行此操作时(经典的 co <​​/ strong>变体分配):

//a real instance that outputs TBig
Outputter<TBig> bigOutputter = new Outputter<TBig>();

//just a view of bigOutputter
ICanOutput<TSmall> smallOutputter = bigOutputter;
  • bigOutputter.getAnInstance()将返回TBig
  • 因为smallOutputter被分配了bigOutputter
    • 在内部,smallOutputter.getAnInstance()将返回TBig
    • TBig可以转换为TSmall
    • 转换已完成,输出为TSmall

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

//a real instance that outputs TSmall
Outputter<TSmall> smallOutputter = new Outputter<TSmall>();

//just a view of smallOutputter
ICanOutput<TBig> bigOutputter = smallOutputter;
  • smallOutputter.getAnInstance()将返回TSmall
  • 因为bigOutputter被分配了smallOutputter
    • 在内部,bigOutputter.getAnInstance()将返回TSmall
    • TSmall无法转换为TBig !!
    • 这是不可能的。
  

这就是&#34; contra 变种&#34;类型不能用作输出类型

你能做些什么&#34;逆变&#34;?

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

假设您有这些类和接口:

interface ICanInput<in T> { bool isInstanceCool(T instance); }

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

再次假设类型TBig继承TSmall。这意味着TBig可以执行TSmall所做的所有事情(它拥有所有TSmall个成员等)。但TSmall无法完成TBig所做的一切(TBig有更多成员)。

执行此操作时(经典反对变体分配):

//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)可以使用smallInstance
    • 中的方法
    • smallAnalyser.isInstanceCool(bigInstance)也可以使用这些方法(它仅查看TSmall的{​​{1}}部分)
  • 自从TBig分配了bigAnalyser以来:
    • 完全可以致电smallAnalyer

如果恰恰相反(好像是 co <​​/ strong>变体):

bigAnalyser.isInstanceCool(bigInstance)
  • //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;
    • bigAnalyser.isInstanceCool可以使用bigAnalyser.isInstanceCool(bigInstance)
    • 中的方法
    • bigInstance无法找到bigAnalyser.isInstanceCool(smallInstance)中的TBig方法!并且无法保证此TSmall甚至是smallInstance已转换。
  • 自从TBig分配了smallAnalyser以来:
    • 调用bigAnalyser会尝试在实例中找到smallAnalyser.isInstanceCool(smallInstance)个方法
    • 并且可能找不到TBig方法,因为此TBig可能不是smallInstance个实例。
  

这就是为什么&#34; co <​​/ em>变种&#34;类型不能用作输入参数

加入

现在,当您添加两个&#34;无法&#34;一起?

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

你能做什么?

我还没有对此进行测试(但是......我想我是否有理由这样做),但似乎没问题,只要你知道你会有一些限制。

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

  • 使用TBig的一个界面,只有不输出in
  • 的方法
  • 使用T的另一个界面只有out作为输入的方法

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

答案 6 :(得分:0)

通用类型参数不能同时是协变和相反的。

为什么?这与inout修饰符施加的限制有关。如果我们想让我们的通用类型参数既协变又是协变的,我们基本上会说:

  • 我们接口的任何方法都不返回T
  • 我们界面的任何方法都不接受T

从本质上讲,这将使我们的通用接口成为非通用接口。

我在另一个question下进行了详细说明: