更新:2007 年 11 月
在 C# 中添加泛型的一个主要好处是能够使用 System.Collections.Generic 命名空间中的类型轻松地创建强类型集合。例如,您可以创建一个类型为 List<int> 的变量,编译器将检查对该变量的所有访问,确保只将 List<int> 添加到该集合中。与 C# 1.0 版中的非类型化集合相比,这是可用性方面的一个很大改进。
遗憾的是强类型集合有自身的缺陷。例如,假设您有一个强类型 List<object>,您想将 List<int> 中的所有元素追加到 List<object> 中。您可能希望能够如下面的示例一样编写代码:
C#
|
List<int> ints = new List<int>(); ints.Add(1); ints.Add(10); ints.Add(42); List<object> objects = new List<object>(); // doesnt compile ints is not a IEnumerable<object> //objects.AddRange(ints); |
在这种情况下,您希望能够将 List<int>(它同时也是 IEnumerable<int>)作为 IEnumerable<object> 处理。这样做看起来似乎很合理,因为 int 可以转换为对象。这与能够将 string[] 当作 object[](现在您就可以这样做)非常相似。如果您正面临这种情况,那么您需要一种称为泛型变化的功能,它将泛型类型的一种实例化(在本例中为 IEnumerable<int>)当成该类型的另一种实例化(在本例中为 IEnumerable<object>)。
由于 C# 不支持泛型类型的变化,所以当遇到这种情况时,您需要尝试几种可能的方法来解决此问题。对于最简单的情况,例如上例中的单个方法 AddRange,您可以声明一个简单的帮助器方法来为您执行转换。例如,您可以编写如下方法:
C#
|
// Simple workaround for single method // Variance in one direction only public static void Add<S, D>(List<S> source, List<D> destination) where S : D { foreach (S sourceElement in source) { destination.Add(sourceElement); } } |
它使您能够完成以下操作:
C#
|
// does compile VarianceWorkaround.Add<int, object>(ints, objects); |
此示例演示了一种简单的变化解决方法的一些特征。帮助器方法带两个类型参数,分别对应于源和目标,源类型参数 S 有一个约束,即目标类型参数 D。这意味着读取的 List<> 所包含的元素必须可以转换为插入的 List<> 类型的元素。这使编译器可以强制 int 可转换为对象。将类型参数约束为从另一类型参数派生被称为裸类型参数约束。
定 义一个方法来解决变化问题不算是一种过于拙劣的方法。遗憾的是变化问题很快就会变得非常复杂。下一级别的复杂性产生在当您想要将一个实例化的接口当作另一 个实例化的接口时。例如,您有一个 IEnumerable<int>,您想将它传递给一个只以 IEnumerable<object> 为参数的方法。同样,这样做也是有一定意义的,因为您可以将 IEnumerable<object> 看作对象的序列,将 IEnumerable<int> 看作 ints 的序列。由于 ints 是对象,因此 ints 的序列应当可以被当作对象序列。例如:
C#
|
static void PrintObjects(IEnumerable<object> objects) { foreach (object o in objects) { Console.WriteLine(o); } } |
您可能希望能够如下面的示例一样调用:
C#
|
// would like to do this, but cant ... // ... ints is not an IEnumerable<object> //PrintObjects(ints); |
接口 case 的解决方法是:创建为接口的每个成员执行转换的包装对象。这可能类似于如下示例:
C#
|
// Workaround for interface // Variance in one direction only so type expressinos are natural public static IEnumerable<D> Convert<S, D>(IEnumerable<S> source) where S : D { return new EnumerableWrapper<S, D>(source); } private class EnumerableWrapper<S, D> : IEnumerable<D> where S : D { |
它使您能够完成以下操作:
C#
|
PrintObjects(VarianceWorkaround.Convert<int, object>(ints));
|
同样,请注意包装类和帮助器方法的裸类型参数约束。此 系统已经变得相当复杂,但是包装类中的代码非常简单;它只委托给所包装接口的成员,除了简单的类型转换外,不执行其他任何操作。为什么不让编译器允许从 IEnumerable<int> 直接转换为 IEnumerable<object> 呢?
尽管在查看集合的 只读视图的情况下,变化是类型安全的,然而在同时涉及读写操作的情况下,变化不是类型安全的。例如,不能用此自动方法处理 IList<> 接口。您仍然可以编写一个帮助器,用类型安全的方式包装 IList<> 上的所有读操作,但是写操作的包装就不能如此轻松了。
下面是处理 IList<(Of <(T>)>) 接口的变化的包装的一部分,它显示在读和写两个方面的变化所引发的问题:
C#
|
private class ListWrapper<S, D> : CollectionWrapper<S, D>, IList<D> where S : D { public ListWrapper(IList<S> source) : base(source) { this.source = source; } public int IndexOf(D item) { if (item is S) { return this.source.IndexOf((S) item); } else { return -1; } } // variance the wrong way ... // ... can throw exceptions at runtime public void Insert(int index, D item) { if (item is S) { this.source.Insert(index, (S)item); } else { throw new Exception("Invalid type exception"); } } |
包装中的 Insert 方法有一个问题。它将 D 当作参数,但是它必须将 D 插入到 IList<S> 中。由于 D 是 S 的基类型,不是所有的 D 都是 S,因此 Insert 操作可能会失败。此示例与数组的变化有相似之处。当将对象插入 object[] 时,将执行动态类型检查,因为 object[] 在运行时可能实际为 string[]。例如:
C#
|
object[] objects = new string[10]; // no problem, adding a string to a string[] objects[0] = "hello"; // runtime exception, adding an object to a string[] objects[1] = new object(); |
在 IList<> 示例中,当实际类型在运行时与需要的类型不匹配时,可以仅仅引发 Insert 方法的包装。所以,您同样可以想象得到编译器将为程序员自动生成此包装。然而,有时候并不应该执行此策略。IndexOf 方法在集合中搜索所提供的项,如果找到该项,则返回该项在集合中的索引。然而,如果没有找到该项,IndexOf 方法将仅仅返回 -1,而并不引发。这种类型的包装不能由自动生成的包装提供。
到目前为止,我们描述了泛型变化问题的两种最简单的解决方法。然而,变化问题 可能变得要多复杂就有多复杂。例如,当您将 List<IEnumerable<int>>当作 List<IEnumerable<object>>,或将 List<IEnumerable<IEnumerable<int>>> 当作 List<IEnumerable<IEnumerable<object>>> 时。
当生成这些包 装以解决代码中的变化问题时,可能给代码带来巨大的系统开销。同时,它还会带来引用标识问题,因为每个包装的标识都与原始集合的标识不一样,从而会导致不 易察觉的 Bug。当使用泛型时,应选择类型实例化,以减少紧密关联的组件之间的不匹配问题。这可能要求在设计代码时做出一些妥协。与往常一样,设计程序时必须权衡 相互冲突的要求,在设计过程中应当考虑语言中类型系统具有的约束。
有的类型系统将泛型变化作为语言的首要任务。Eiffel 是其中一个主要示例。然而,将泛型变化作为类型系统的首要任务会明显增加 C# 的类型系统的复杂性,即使在不涉及变化的相对简单方案中也是如此。因此,C# 的设计人员觉得不包括变化才是 C# 的适当选择。
下面是上述示例的完整源代码。
C#
|
using System; using System.Collections.Generic; using System.Text; using System.Collections; static class VarianceWorkaround { // Simple workaround for single method // Variance in one direction only public static void Add<S, D>(List<S> source, List<D> destination) where S : D { foreach (S sourceElement in source) { destination.Add(sourceElement); } } // Workaround for interface // Variance in one direction only so type expressinos are natural public static IEnumerable<D> Convert<S, D>(IEnumerable<S> source) where S : D { return new EnumerableWrapper<S, D>(source); } private class EnumerableWrapper<S, D> : IEnumerable<D> where S : D { public EnumerableWrapper(IEnumerable<S> source) { this.source = source; } public IEnumerator<D> GetEnumerator() { return new EnumeratorWrapper(this.source.GetEnumerator()); } IEnumerator System.Collections.IEnumerable.GetEnumerator() { return this.GetEnumerator(); } private class EnumeratorWrapper : IEnumerator<D> { public EnumeratorWrapper(IEnumerator<S> source) { this.source = source; } private IEnumerator<S> source; public D Current { get { return this.source.Current; } } public void Dispose() { this.source.Dispose(); } object IEnumerator.Current { get { return this.source.Current; } } public bool MoveNext() { return this.source.MoveNext(); } public void Reset() { this.source.Reset(); } } private IEnumerable<S> source; } // Workaround for interface // Variance in both directions, causes issues // similar to existing array variance public static ICollection<D> Convert<S, D>(ICollection<S> source) where S : D { return new CollectionWrapper<S, D>(source); } private class CollectionWrapper<S, D> : EnumerableWrapper<S, D>, ICollection<D> where S : D { public CollectionWrapper(ICollection<S> source) : base(source) { } // variance going the wrong way ... // ... can yield exceptions at runtime public void Add(D item) { if (item is S) { this.source.Add((S)item); } else { throw new Exception(@"Type mismatch exception, due to type hole introduced by variance."); } } public void Clear() { this.source.Clear(); } // variance going the wrong way ... // ... but the semantics of the method yields reasonable semantics public bool Contains(D item) { if (item is S) { return this.source.Contains((S)item); } else { return false; } } // variance going the right way ... public void CopyTo(D[] array, int arrayIndex) { foreach (S src in this.source) { array[arrayIndex++] = src; } } public int Count { get { return this.source.Count; } } public bool IsReadOnly { get { return this.source.IsReadOnly; } } // variance going the wrong way ... // ... but the semantics of the method yields reasonable semantics public bool Remove(D item) { if (item is S) { return this.source.Remove((S)item); } else { return false; } } private ICollection<S> source; } // Workaround for interface // Variance in both directions, causes issues similar to existing array variance public static IList<D> Convert<S, D>(IList<S> source) where S : D { return new ListWrapper<S, D>(source); } private class ListWrapper<S, D> : CollectionWrapper<S, D>, IList<D> where S : D { public ListWrapper(IList<S> source) : base(source) { this.source = source; } public int IndexOf(D item) { if (item is S) { return this.source.IndexOf((S) item); } else { return -1; } } // variance the wrong way ... // ... can throw exceptions at runtime public void Insert(int index, D item) { if (item is S) { this.source.Insert(index, (S)item); } else { throw new Exception("Invalid type exception"); } } public void RemoveAt(int index) { this.source.RemoveAt(index); } public D this[int index] { get { return this.source[index]; } set { if (value is S) this.source[index] = (S)value; else throw new Exception("Invalid type exception."); } } private IList<S> source; } } namespace GenericVariance { class Program { static void PrintObjects(IEnumerable<object> objects) { foreach (object o in objects) { Console.WriteLine(o); } } static void AddToObjects(IList<object> objects) { // this will fail if the collection provided is a wrapped collection objects.Add(new object()); } static void Main(string[] args) { List<int> ints = new List<int>(); ints.Add(1); ints.Add(10); ints.Add(42); List<object> objects = new List<object>(); // doesnt compile ints is not a IEnumerable<object> //objects.AddRange(ints); // does compile VarianceWorkaround.Add<int, object>(ints, objects); // would like to do this, but cant ... // ... ints is not an IEnumerable<object> //PrintObjects(ints); PrintObjects(VarianceWorkaround.Convert<int, object>(ints)); AddToObjects(objects); // this works fine AddToObjects(VarianceWorkaround.Convert<int, object>(ints)); } static void ArrayExample() { object[] objects = new string[10]; // no problem, adding a string to a string[] objects[0] = "hello"; // runtime exception, adding an object to a string[] objects[1] = new object(); } } } |