C#方差问题:将List <Derived>分配为List <Base>

And*_*Dog 54 c# covariance

请看以下示例(部分取自MSDN博客):

class Animal { }
class Giraffe : Animal { }

static void Main(string[] args)
{
    // Array assignment works, but...
    Animal[] animals = new Giraffe[10]; 

    // implicit...
    List<Animal> animalsList = new List<Giraffe>();

    // ...and explicit casting fails
    List<Animal> animalsList2 = (List<Animal>) new List<Giraffe>();
}
Run Code Online (Sandbox Code Playgroud)

这是一个协方差问题吗?这将在未来的C#版本中得到支持吗?是否有任何聪明的解决方法(仅使用.NET 2.0)?

Jon*_*eet 112

那么这肯定不会在C#4中得到支持.有一个根本问题:

List<Giraffe> giraffes = new List<Giraffe>();
giraffes.Add(new Giraffe());
List<Animal> animals = giraffes;
animals.Add(new Lion()); // Aargh!
Run Code Online (Sandbox Code Playgroud)

保持长颈鹿安全:只对不安全的方差说不.

数组版本有效,因为数组确实支持引用类型方差,并且执行时间检查.泛型的关键是提供编译时类型的安全性.

在C#4中,将支持安全通用方差,但仅适用于接口和委托.所以你将能够做到:

Func<string> stringFactory = () => "always return this string";
Func<object> objectFactory = stringFactory; // Safe, allowed in C# 4
Run Code Online (Sandbox Code Playgroud)

Func<out T>协变T,因为T在输出位置时才使用.比较Action<in T>哪个是逆变T因为因为T只用在那里的输入位置,使这个安全:

Action<object> objectAction = x => Console.WriteLine(x.GetHashCode());
Action<string> stringAction = objectAction; // Safe, allowed in C# 4
Run Code Online (Sandbox Code Playgroud)

IEnumerable<out T> 如同其他人所指出的那样,在C#4中也是正确的,这是正确的:

IEnumerable<Animal> animals = new List<Giraffe>();
// Can't add a Lion to animals, as `IEnumerable<out T>` is a read-only interface.
Run Code Online (Sandbox Code Playgroud)

在C#2的情况下解决这个问题,你需要维护一个列表,还是开心创建一个新列表?如果那是可以接受的,那List<T>.ConvertAll就是你的朋友.

  • @Jeff:因为`animals`*实际上*指的是`List <Giraffe>`.所以你最终会在`List <Giraffe>`中找到一个`Lion`引用,这很糟糕...... (3认同)
  • `animals.Add(new Lion())有什么问题; //啊啊!如果你可以施放一个"狮子"并将其用作"动物",如果你使用"动物"中的所有元素作为"动物",那么问题是什么? (2认同)
  • 值得一提的是,更适合List <T>的可能是IReadOnlyList <out T>,它也是安全协变的,是不可变的. (2认同)
  • @AlexandreDaubricourt:但是其他一些代码可以引用与“List &lt;Giraffe&gt;”相同的对象(如我的示例中所示)。因此,当尝试执行任何操作时,您会收到错误,但这可能会晚很多很多。IMO,最好尽早发现问题。 (2认同)

Lee*_*Lee 16

它将在C#4中工作IEnumerable<T>,所以你可以这样做:

IEnumerable<Animal> animals = new List<Giraffe>();
Run Code Online (Sandbox Code Playgroud)

但是,List<T>这不是一个协变量投影,所以你不能像上面那样分配列表,因为你可以这样做:

List<Animal> animals = new List<Giraffe>();
animals.Add(new Monkey());
Run Code Online (Sandbox Code Playgroud)

这显然无效.


Nol*_*rin 9

List<T>我而言,我担心你运气不好.但是,.NET 4.0/C#4.0增加了对协变/逆变接口的支持.具体来说,IEnumerable<T>现在定义为IEnumerable<out T>,这意味着类型参数现在是协变的.

这意味着您可以在C#4.0中执行类似的操作...

// implicit casting
IEnumerable<Animal> animalsList = new List<Giraffe>();

// explicit casting
IEnumerable<Animal> animalsList2 = (IEnumerable<Animal>) new List<Giraffe>();
Run Code Online (Sandbox Code Playgroud)

注意:数组类型也是协变的(至少从.NET 1.1开始).

我认为很遗憾的是,没有添加方差支持IList<T>和其他类似的通用接口(或通用类甚至),但是哦,至少我们有一些东西.

  • 您无法使用声明方差异注释安全地使IList <T>协变. (9认同)

Aar*_*ght 5

其他人提到的可变集合不能支持协方差/逆变,因为在编译时不可能保证类型安全; 但是,如果您正在寻找的话,可以在C#3.5中进行快速单向转换:

List<Giraffe> giraffes = new List<Giraffe>();
List<Animal> animals = giraffes.Cast<Animal>().ToList();
Run Code Online (Sandbox Code Playgroud)

当然它不是同一个东西,它实际上不是协方差 - 你实际上是在创建另一个列表,但它可以说是"解决方法".

在.NET 2.0中,您可以利用数组协方差来简化代码:

List<Giraffe> giraffes = new List<Giraffe>();
List<Animal> animals = new List<Animal>(giraffes.ToArray());
Run Code Online (Sandbox Code Playgroud)

但请注意,您实际上是在这里创建两个新集合.