协变在C#
是否有可能在C#4.0中将List<Subclass>
List<Superclass>
为List<Superclass>
?
沿着这些线路的东西:
class joe : human {} List<joe> joes = GetJoes(); List<human> humanJoes = joes;
这不是协变的原因吗?
如果你能做到:
human h = joe1 as human;
你为什么不能做呢
List<human> humans = joes as List<human>;
比(joe)人类[0]是不合法的,因为这个项目已经被拒绝了,每个人都会很高兴。 现在唯一的select是创build一个新的列表
你不能这样做,因为它不安全。 考虑:
List<Joe> joes = GetJoes(); List<Human> humanJoes = joes; humanJoes.Clear(); humanJoes.Add(new Fred()); Joe joe = joes[0];
显然,最后一行(如果不是更早的) 必须失败 – 因为Fred
不是Joe
。 List<T>
的不变性在编译时防止了这个错误,而不是执行时间。
实例化一个将joes作为input的新人类列表:
List<human> humanJoes = new List<human>(joes);
不可以。C#4.0的协变特性只支持接口和委托。 不支持像List<T>
这样的具体types。
不,正如Jared所说,C#4.0的协变特性只支持接口和委托。 然而它也不适用于IList<T>
,原因是IList<T>
包含添加和更改列表中的项目的方法 – 正如Jon Skeet的新答案所述。
能够将“joe”列表转换为“human”的唯一方法是如果接口纯粹是通过devise只读的,如下所示:
public interface IListReader<out T> : IEnumerable<T> { T this[int index] { get; } int Count { get; } }
即使是Contains(T item)
方法也是不允许的,因为当你将IListReader<joe>
投射到IListReader<human>
, IListReader<joe>
中没有Contains(human item)
方法。
你可以用一个GoInterface从IList<joe>
IListReader<joe>
到IListReader<joe>
, IListReader<human>
甚至IList<human>
。 但是,如果列表足够小以便复制,则更简单的解决scheme就是将其复制到新的List<human>
,正如Paw指出的那样。
如果我允许你的List<Joe> joes
被推广为…
List<Human> humans = joes;
…现在,两个参考joes
和joes
指向完全相同的列表。 上述分配之后的代码没有办法阻止将另一种types的人(例如pipe道工)的实例插入/添加到列表中。 鉴于class Plumber: Human {}
humans.Add(new Plumber()); // Add() now accepts any Human not just a Joe
现在humans
引用的列表包含乔和水pipe工。 请注意,相同的列表对象仍由引用joes
引用。 现在,如果我使用参考joes
从列表对象读取,我可能会popup一个水pipe工,而不是一个乔 。 水pipe工和乔不知道是隐式的相互转换…所以我从一个水pipe工,而不是从名单上的乔得到打破types安全。 一个水pipe工当然不欢迎通过参考一个乔的名单。
然而,在最近的C#版本中,通过实现一个generics接口,其types参数有一个out
修饰符,可以解决generics类/集合的这种限制。 假设我们现在有ABag<T> : ICovariable<out T>
。 out修饰符仅将T限制在输出位置(例如方法返回types)。 你不能input任何T到袋子里。 你只能读出来。 这允许我们将joes概括为一个ICovariable<Human>
而不用担心插入水pipe工,因为接口不允许这样做。 我们现在可以写…
ICovariable<Human> humans = joes ; // now its good ! humans.Add(new Plumber()); // error