【问题标题】:Simple examples of co and contravariance协变和逆变的简单例子
【发布时间】:2011-01-12 14:25:44
【问题描述】:

有人可以给我提供简单的 C# 示例,如协变、逆变、不变和反不变(如果存在的话)。

到目前为止我看到的所有样本都只是将一些对象投射到System.Object

【问题讨论】:

  • 有人将对象转换为System.Object 作为协方差的示例?这根本不对。
  • 我认为这意味着传递某种对象(具有某种特定的、更派生的类型)代替System.Object,不一定将object 转换为System.Object,这毫无意义。
  • Variance 不应与 casting 混淆,它们不是一回事。请参阅:Difference between covariance and upcasting

标签: c# .net covariance contravariance invariants


【解决方案1】:

有人可以给我提供简单的 C# 示例,例如协变、逆变、不变和反不变(如果存在的话)。

我不知道“反不变性”是什么意思。其余的都很简单。

这是一个协方差的例子:

void FeedTheAnimals(IEnumerable<Animal> animals) 
{ 
    foreach(Animal animal in animals)
        animal.Feed();
}
...
List<Giraffe> giraffes = ...;
FeedTheAnimals(giraffes);

IEnumerable&lt;T&gt; 接口是协变的。 Giraffe 可转换为 Animal 的事实意味着 IEnumerable&lt;Giraffe&gt; 可转换为 IEnumerable&lt;Animal&gt;。由于List&lt;Giraffe&gt; 实现IEnumerable&lt;Giraffe&gt;,此代码在C# 4 中成功;它在 C# 3 中会失败,因为 IEnumerable&lt;T&gt; 上的协方差在 C# 3 中不起作用。

这应该是有道理的。一个长颈鹿序列可以被视为一个动物序列。

这是一个逆变的例子:

void DoSomethingToAFrog(Action<Frog> action, Frog frog)
{
    action(frog);
}
...
Action<Animal> feed = animal=>{animal.Feed();}
DoSomethingToAFrog(feed, new Frog());

Action&lt;T&gt; 委托是逆变的。 Frog 可转换为 Animal 的事实意味着 Action&lt;Animal&gt; 可转换为 Action&lt;Frog&gt;。注意这种关系是如何与协变关系相反的;这就是为什么它是“对立”变体。由于可兑换,此代码成功;在 C# 3 中它会失败。

这应该是有道理的。该动作可以采取任何动物;我们需要一个可以带走任何 Frog 的动作,一个可以带走任何 Animal 的动作肯定也可以带走任何 Frog。

不变性的一个例子:

void ReadAndWrite(IList<Mammal> mammals)
{
    Mammal mammal = mammals[0];
    mammals[0] = new Tiger();
}

我们可以将IList&lt;Giraffe&gt; 传递给这个东西吗?不,因为有人要在其中写入老虎,而老虎不能出现在长颈鹿列表中。我们可以将IList&lt;Animal&gt; 传递给这个东西吗?不,因为我们要从中读取哺乳动物,而动物列表可能包含一只青蛙。 IList&lt;T&gt;不变的。只能按实际情况使用。

有关此功能设计的其他想法,请参阅我关于我们如何设计和构建它的系列文章。

http://blogs.msdn.com/b/ericlippert/archive/tags/covariance+and+contravariance/

【讨论】:

  • 在您的最后一个示例中可能值得指出,当然,您可以Giraffe[] 转换为Mammal[] 并将其传入,这将导致出现运行时错误。
  • @Dan:好点子。 C# 支持“损坏的”数组协方差,其中允许一些协变转换,即使它们会在运行时导致崩溃。
  • @Eric:是的,我绝对理解那里的推理(我认为):开发人员通常想要T[](或IList&lt;T&gt;)的接口的“子集”,以通过以下方式访问元素不打算写入集合的索引。这就是为什么我个人非常坚信 BCL 中应该有一个像协变 IArray&lt;T&gt; 接口和索引 getter 这样的东西。这将允许List&lt;Giraffe&gt;IList&lt;Mammal&gt; 一样在它仅用于索引访问而不是它的可变特性(即,在Giraffe[] 可以安全地充当Mammal[] 的情况下)。
  • @Dan:或者,一个实际不可变的数组类型,保证在其初始化后不可写,将是安全协变的。我很想拥有这样的东西。数组是 变量 的集合这一事实经常令人烦恼。我需要一个的集合。
  • 是数组协方差为什么new ReadOnlyCollection&lt;Animal&gt;(new Frog[] { new Frog() }); 可以编译(并且可以工作)但new ReadOnlyCollection&lt;Animal&gt;(new List&lt;Frog&gt; { new Frog() }); 不能? (本例中,构造函数参数为IList&lt;Animal&gt;)。
【解决方案2】:

不变性(在这种情况下)是不存在协变和反变。所以反不变性这个词没有意义。任何未标记为inout 的类型参数都是不变的。这意味着这个类型参数既可以被消费也可以被返回。

协方差的一个很好的例子是IEnumerable&lt;out T&gt;,因为IEnumerable&lt;Derived&gt; 可以代替IEnumerable&lt;Base&gt;。或者 Func&lt;out T&gt; 返回 T 类型的值。
例如,IEnumerable&lt;Dog&gt; 可以转换为 IEnumerable&lt;Animal&gt;,因为任何 Dog 都是动物。

对于逆变器,您可以使用任何消费接口或委托。我想到了IComparer&lt;in T&gt;Action&lt;in T&gt;。这些从不返回T 类型的变量,只接收它。无论您希望在哪里收到Base,您都可以传入Derived

将它们视为仅输入或仅输出类型参数可以更容易地理解 IMO。

invariants 这个词通常不与类型变化一起使用,而是在类或方法不变量的上下文中使用,并表示一个保守的属性。请参阅this stackoverflow thread,其中讨论了不变量和不变性之间的差异。

【讨论】:

  • 对吗?“无论您希望在哪里收到一个基,您都可以传入一个派生”?
【解决方案3】:

如果你考虑泛型的常规使用,你经常使用一个接口来处理一个对象,但是这个对象是一个类的一个实例——你不能实例化这个接口。以简单的字符串列表为例。

IList<string> strlist = new List<string>();

我相信您知道使用IList&lt;&gt; 而不是直接使用List&lt;&gt; 的优势。它允许控制反转,您可能会决定不再使用List&lt;&gt;,而是需要LinkedList&lt;&gt;。上面的代码运行良好,因为接口和类的泛型类型相同:string

如果你想创建一个字符串列表,它可能会变得更复杂一些。考虑这个例子:

IList<IList<string>> strlists = new List<List<string>>();

这显然不会编译,因为泛型类型参数IList&lt;string&gt;List&lt;string&gt; 不一样。即使您将外部列表声明为常规类,例如 List&lt;IList&lt;string&gt;&gt;,它也不会编译 - 类型参数不匹配。

所以这里是协方差可以提供帮助的地方。协方差允许您在此表达式中使用更多派生类型作为类型参数。如果IList&lt;&gt; 是协变的,它将简单地编译并修复问题。不幸的是,IList&lt;&gt; 不是协变的,但它扩展的接口之一是:

IEnumerable<IList<string>> strlists = new List<List<string>>();

此代码现在可以编译,类型参数与上面相同。

【讨论】:

    猜你喜欢
    • 2012-01-18
    • 2011-02-09
    • 1970-01-01
    • 2016-11-03
    • 2015-02-09
    • 2012-04-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多