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;
答案 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)
通用类型参数不能同时是协变和相反的。
为什么?这与in
和out
修饰符施加的限制有关。如果我们想让我们的通用类型参数既协变又是协变的,我们基本上会说:
从本质上讲,这将使我们的通用接口成为非通用接口。
我在另一个question下进行了详细说明: