11

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; 
4

7 回答 7

25

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

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

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

根据定义,如果一个类型是“协变有效的”,那么它就不能以逆变方式使用。如果它是“逆变有效的”,那么它就不能以协变方式使用。协变有效和逆变有效的东西不能协变或逆变方式使用。也就是说,它是不变的。所以,有变和逆变的联合:它们的联合是不变的。

其次,让我们假设您实现了自己的愿望,并且有一个类型注释可以按照我认为您想要的方式工作:

interface IBurger<in and out T> {}

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

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

于 2010-12-25T01:42:01.353 回答
8

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

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

于 2010-12-24T20:35:01.583 回答
5

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

如果你创建了一个类型参数既是协变的又是逆变的,你不能输入它也不能输出它——你根本不能使用它。

于 2010-12-24T20:36:43.310 回答
1

没有 out 和 in 关键字参数是协变和逆变不是吗?

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

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

没有 in 和 out 意味着它可以用作参数类型和返回值类型

于 2010-12-24T20:31:49.150 回答
0

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

不,有一个基于基本逻辑(或只是常识,随你喜欢)的更简单的原因:一个陈述不能同时是真实的和不真实的。

协变意味着S <: T ⇒ G<S> <: G<T>和逆变意味着S <: T ⇒ G<T> <: G<S>。很明显,这些不可能同时为真。

于 2010-12-25T00:15:15.130 回答
0

你可以用“协变”做什么?

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

假设你有这些类和接口:

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

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

现在假设你有继承的TBig类型TSmall。这意味着一个TBig实例也总是一个TSmall实例;但TSmall实例并不总是TBig实例。(名称的选择是为了便于在TSmall内部可视化TBig

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

//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!!
    • 那么这是不可能的。

这就是为什么对立变体”类型不能用作输出类型 的原因


你可以用“逆变器”做什么?

遵循上面相同的想法,逆变使用修饰符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不能做所有事情TBigTBig有更多成员)。

当您执行此操作时(经典的contra变体分配):

//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部分TBig
  • 由于bigAnalyser被分配smallAnalyer
    • 完全可以打电话bigAnalyser.isInstanceCool(bigInstance)

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

//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可能不是一个TBig实例。

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


加入两者

现在,当您将两个“不能”加在一起时会发生什么?

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

你能做什么?

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

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

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

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

于 2018-04-19T15:42:01.080 回答
0

泛型类型参数不能既是协变的又是逆变的。

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

  • 我们接口的所有方法都没有返回 T
  • 我们接口的所有方法都不接受 T

这本质上会使我们的通用接口非通用。

我在另一个问题下详细解释了它:

于 2018-11-03T20:06:08.673 回答