8

我已经听到/阅读了很多关于 C# 中的协方差问题的内容,我想提出一些问题和场景,希望我能澄清我对此事的困惑。

在这些示例中,请假设始终定义以下内容:

public class Apple : Fruit {}

我的第一个例子:

IList<Apple> apples = GetApples();
IList<Fruit> fruits = apples;

这应该有效,对吗?我在 C# 中对此进行了几次测试,它编译得很好并且运行良好(我对第一个示例的测试比这略多,因为我有将内容打印到控制台的多态调用)。

第二个例子:

IList<Apple> apples = GetApples();
IList<object> fruits = apples;

在第二个示例中,我的理解是这不应该编译,并且是 .NET 4.0 中解决的协方差问题的根源。如果我错了,请纠正我。我也知道.NET 4.0 不允许具体类型之间的协变/逆变,只有接口。

最后,我想得到一些定义。我不太清楚这三个术语背后的含义:

  • 协方差
  • 逆变
  • 不变性(与不变性相同?)

至于最后一个词,我在 C++ 中经常使用它来指代隐含规则的更改。例如,如果我有一个整数并且它只允许有 1 到 10 之间的值,那么“不变性”就是它只能在 1 到 10 之间。我可能会误解这一点,我也不确定是否对于此特定讨论,此定义很好地转换为 C#。

编辑

我的目标是准确了解 C# 中泛型接口的协方差或转换问题。我发布的示例是我对问题所在的理解。如果所有示例都编译/运行良好,请提供一个示例,该示例确实重现了 C# 中最常见的协变/逆变/转换问题。我需要知道这一点,以便我可以识别并向其他人解释问题。

4

2 回答 2

6

IList<T>接口未定义为协变的,因为它支持Add使对象发生变异的方法。

考虑以下:

IList<Apple> apples = GetApples();
IList<object> fruits = apples;
fruits.Add(new Banana());

你现在可以得到一个Bananafrom apples,这肯定不是你想要的。因此,IList接口不支持协方差(而且永远不会),并且应该会导致编译错误。

你应该有同样的问题

IList<Apple> apples = GetApples();
IList<Fruit> fruits = apples;
fruits.Add(new Banana());

所以我不确定为什么要为你编译。

IEnumerable<out T>接口可以是协变的(在 .NET 4.0 及更高版本中),因为IEnumerable仅支持从集合中读取元素。


Scala 语言有类似的协变和逆变对象概念,Scala 编程中讨论泛型的章节也应该很好地介绍 C# 中的协变。

于 2011-07-05T00:48:16.157 回答
2

查看这篇文章以了解协变和逆变的解释。

http://msdn.microsoft.com/en-us/library/dd799517.aspx


CLR 已经对泛型类型的变化提供了一些支持,并且随着 c# 4 提供了使用它的语法。对于泛型变体,变体适用于接口和委托类型的类型参数。

协方差是关于能够将返回值视为更通用的类型,并且当接口方法仅返回该类型时是可能的。在此示例中,派生接口实例可以重新分配为基础,但不能反过来。

public interface ISomeInterfaceWithOut<out T>
{
    T GetSomething();
}

ISomeInterfaceWithOut<Apple> b = new Blah<Apple>();
ISomeInterfaceWithOut<Fruit> fruit = b;

逆变是关于能够将参数类型视为更具体的类型,并且当接口方法仅使用该类型时是可能的。在此示例中,可以将基本接口实例重新分配为派生实例,但不能反过来。

public interface ISomeInterfaceWithIn<in T>
{
    void SetSomething(T instance);
}

ISomeInterfaceWithIn<Fruit> b = new Blah<Fruit>();
ISomeInterfaceWithIn<Apple> apple = b;

不变性是当这两种情况都发生并且接口方法同时返回和使用类型时。协变或逆变都不能应用。在这里,像上面这样的任何用法都不起作用,因为不允许定义“out T”协方差或“in T”逆变类型参数,因为方法包含这两种情况。

考虑一下:

//it is not possible to declare 'out T' or 'in T' here - invalid variance
public interface ISomeInterface<T>
{
    T GetSomething();
    void SetSomething(T instance);
}

您的两个示例都不会按原样工作。逆变/协变适用于已将其泛型类型声明为“in”/“out”的接口和委托,IList 是不变的。

由于该IEnumerable<T>接口与 .NET 4 是协变的,因此您可以从 4 开始执行此操作,但不能从 3.5 执行此操作。在声明水果时,在这里使用水果作为 IList 将不起作用 - 它不是协变的。

List<Apple> apples = new List<Apple>();
//List<Apple> apples implements IEnumerable<Apple>
IEnumerable<Fruit> fruits = apples;

这里是定义IEnumerable<T>

//Version=4.0.0.0
public interface IEnumerable<out T> : IEnumerable
{
    new IEnumerator<T> GetEnumerator();
}

//Version=2.0.0.0
public interface IEnumerable<T> : IEnumerable
{
    new IEnumerator<T> GetEnumerator();
}
于 2011-07-05T01:01:54.760 回答