泛型 - 2
泛型代码中的默认关键字(C# 编程指南)
在泛型类和泛型方法中产生的一个问题是,在预先未知以下情况时,如何将默认值分配给参数化类型 T:
-
T 是引用类型还是值类型。
-
如果 T 为值类型,则它是数值还是结构。
给定参数化类型 T 的一个变量 t,只有当 T 为引用类型时,语句 t = null 才有效;只有当 T 为数值类型而不是结构时,语句 t = 0 才能正常使用。解决方案是使用 default 关键字,此关键字对于引用类型会返回空,对于数值类型会返回零。对于结构,此关键字将返回初始化为零或空的每个结构成员,具体取决于这些结构是值类型还是引用类型。以下来自 GenericList<T> 类的示例显示了如何使用 default 关键字。有关更多信息,请参见泛型概述。
public class GenericList<T> { private class Node { //... public Node Next; public T Data; } private Node head; //... public T GetNext() { T temp = default(T); Node current = head; if (current != null) { temp = current.Data; current = current.Next; } return temp; } }
C++ 模板和 C# 泛型之间的区别(C# 编程指南)
C# 泛型和 C++ 模板都是用于提供参数化类型支持的语言功能。然而,这两者之间存在许多差异。在语法层面上,C# 泛型是实现参数化类型的更简单方法,不具有 C++ 模板的复杂性。此外,C# 并不尝试提供 C++ 模板所提供的所有功能。在实现层面,主要区别在于,C# 泛型类型替换是在运行时执行的,从而为实例化的对象保留了泛型类型信息。有关更多信息,请参见运行库中的泛型(C# 编程指南)。
以下是 C# 泛型和 C++ 模板之间的主要差异:
-
C# 泛型未提供与 C++ 模板相同程度的灵活性。例如,尽管在 C# 泛型类中可以调用用户定义的运算符,但不能调用算术运算符。
-
C# 不允许非类型模板参数,如 template C<int i> {}。
-
C# 不支持显式专用化,即特定类型的模板的自定义实现。
-
C# 不支持部分专用化:类型参数子集的自定义实现。
-
C# 不允许将类型参数用作泛型类型的基类。
-
C# 不允许类型参数具有默认类型。
-
在 C# 中,尽管构造类型可用作泛型,但泛型类型参数自身不能是泛型。C++ 确实允许模板参数。
-
C++ 允许那些可能并非对模板中的所有类型参数都有效的代码,然后将检查该代码中是否有用作类型参数的特定类型。C# 要求相应地编写类中的代码,使之能够使用任何满足约束的类型。例如,可以在 C++ 中编写对类型参数的对象使用算术运算符 + 和 - 的函数,这会在使用不支持这些运算符的类型来实例化模板时产生错误。C# 不允许这样;唯一允许的语言构造是那些可从约束推导出来的构造。
运行库中的泛型(C# 编程指南)
将泛型类型或方法编译为 Microsoft 中间语言 (MSIL) 时,它包含将其标识为具有类型参数的元数据。泛型类型的 MSIL 的使用因所提供的类型参数是值类型还是引用类型而不同。
第一次用值类型作为参数来构造泛型类型时,运行库会创建专用泛型类型,将提供的参数代入到 MSIL 中的适当位置。对于每个用作参数的唯一值类型,都会创建一次专用泛型类型。
例如,假设您的程序代码声明了一个由整数构造的堆栈,如下所示:
Stack<int> stack;
在此位置,运行库生成 Stack <T> 类的专用版本,并相应地用整数替换其参数。现在,只要程序代码使用整数堆栈,运行库就会重用生成的专用 Stack<T> 类。在下面的示例中,创建了整数堆栈的两个实例,它们共享 Stack<int> 代码的单个实例:
Stack<int> stackOne = new Stack<int>(); Stack<int> stackTwo = new Stack<int>();
但是,如果在程序代码中的其他位置创建了另一个 Stack<T> 类,这次使用不同的值类型(如 long 或用户定义的结构)作为其参数,则运行库会生成泛型类型的另一版本(这次将在 MSIL 中的适当位置代入 long)。由于每个专用泛型类本身就包含值类型,因此不再需要转换。
对于引用类型,泛型的工作方式略有不同。第一次使用任何引用类型构造泛型类型时,运行库会创建专用泛型类型,用对象引用替换 MSIL 中的参数。然后,每次使用引用类型作为参数来实例化构造类型时,无论引用类型的具体类型是什么,运行库都会重用以前创建的泛型类型的专用版本。之所以可以这样,是因为所有引用的大小相同。
例如,假设您有两个引用类型:一个 Customer 类和一个 Order 类,并且进一步假设您创建了一个 Customer 类型的堆栈:
class Customer { } class Order { }
Stack<Customer> customers;
在此情况下,运行库生成 Stack<T> 类的一个专用版本,该版本不是存储数据,而是存储稍后将填写的对象引用。假设下一行代码创建另一个引用类型的堆栈,称为 Order:
Stack<Order> orders = new Stack<Order>();
不同于值类型,对于 Order 类型不创建 Stack<T> 类的另一个专用版本。而是创建 Stack<T> 类的一个专用版本实例,并将 orders 变量设置为引用它。假设接下来您遇到一行创建 Customer 类型堆栈的代码:
customers = new Stack<Customer>();
与前面使用 Order 类型创建的 Stack<T> 类一样,创建了专用 Stack<T> 类的另一个实例,并且其中所包含的指针被设置为引用 Customer 类型大小的内存区域。因为引用类型的数量会随程序的不同而大幅变化,C# 泛型实现将编译器为引用类型的泛型类创建的专用类的数量减小到一个,从而大幅减小代码量的增加。
此外,使用类型参数实例化泛型 C# 类时,无论它是值类型还是引用类型,可以在运行时使用反射查询它,并且可以确定它的实际类型和类型参数。
.NET Framework 类库中的泛型(C# 编程指南)
.NET Framework 2.0 版类库提供一个新的命名空间 System.Collections.Generic,其中包括几个随时可用的泛型集合类和关联接口。其他命名空间(如 System)也提供新的泛型接口,如 IComparable<T>。与早期版本的 .NET Framework 所提供的非泛型集合类相比,这些类和接口更为高效和类型安全。在设计和实现自己的自定义集合类之前,请考虑是否能够使用基类库所提供的类,或是否能从基类库所提供的类派生。
泛型和反射(C# 编程指南)
因为公共语言运行库 (CLR) 能够在运行时访问泛型类型信息,所以可以使用反射获取关于泛型类型的信息,方法与用于非泛型类型的方法相同。有关更多信息,请参见运行库中的泛型(C# 编程指南)。
在 .NET Framework 2.0 中,Type 类增添了几个新成员以启用泛型类型的运行时信息。有关如何使用这些方法和属性的更多信息,请参见关于这些类的文档。System.Reflection.Emit 命名空间还包含支持泛型的新成员。请参见如何:定义具有反射发出的泛型类型。
有关泛型反射中使用的术语的固定条件列表,请参见 IsGenericType 属性备注。
System.Type 成员名称 | 说明 |
---|---|
IsGenericType |
如果类型为泛型,则返回 true。 |
返回 Type 对象数组,这些对象表示为构造类型提供的类型变量,或泛型类型定义的类型参数。 |
|
返回当前构造类型的基础泛型类型定义。 |
|
返回表示当前泛型类型参数约束的 Type 对象的数组。 |
|
如果类型或其任意封闭类型或方法包含没有被提供特定类型的类型参数,则返回 true。 |
|
获取 GenericParameterAttributes 标志的组合,这些标志描述当前泛型类型参数的特殊约束。 |
|
对于表示类型参数的 Type 对象,获取类型参数在声明该类型参数的泛型类型定义或泛型方法定义的类型参数列表中的位置。 |
|
获取一个值,该值指示当前 Type 是表示泛型类型定义的类型参数,还是泛型方法定义的类型参数。 |
|
获取一个值,该值指示当前 Type 是否表示可以用来构造其他泛型类型的泛型类型定义。如果类型表示泛型类型的定义,则返回 true。 |
|
返回定义当前泛型类型参数的泛型方法;如果类型参数不是由泛型方法定义的,则返回空值。 |
|
用类型数组的元素替代当前泛型类型定义的类型参数,并返回表示结果构造类型的 Type 对象。 |
此外,MethodInfo 类中还添加了新成员以启用泛型方法的运行时信息。有关泛型方法反射中使用的术语的固定条件列表,请参见 IsGenericMethod 属性备注。
System.Reflection.MemberInfo 成员名称 | 说明 |
---|---|
IsGenericMethod |
如果方法为泛型,则返回 true。 |
返回 Type 对象数组,这些对象表示构造泛型方法的类型变量,或泛型方法定义的类型参数。 |
|
返回当前构造方法的基础泛型方法定义。 |
|
如果方法或其任意封闭类型包含没有被提供特定类型的任何类型参数,则返回 true。 |
|
如果当前 MethodInfo 表示泛型方法的定义,则返回 true。 |
|
用类型数组的元素替代当前泛型方法定义的类型参数,并返回表示结果构造方法的 MethodInfo 对象。 |
泛型和属性(C# 编程指南)
属性可以应用于泛型类型中,方式与应用于非泛型类型相同。有关应用属性的更多信息,请参见属性(C# 编程指南)。
自定义属性只允许引用开放泛型类型(未提供类型参数的泛型类型)和封闭构造泛型类型(为所有类型参数提供参数)。
下面的示例使用此自定义属性:
class CustomAttribute : System.Attribute { public System.Object info; }
属性可以引用开放式泛型类型:
public class GenericClass1<T> { } [CustomAttribute(info = typeof(GenericClass1<>))] class ClassA { }
使用数目适当的若干个逗号指定多个类型参数。在此示例中,GenericClass2 有两个类型参数:
public class GenericClass2<T, U> { } [CustomAttribute(info = typeof(GenericClass2<,>))] class ClassB { }
属性可以引用封闭式构造泛型类型:
public class GenericClass3<T, U, V> { } [CustomAttribute(info = typeof(GenericClass3<int, double, string>))] class ClassC { }
引用泛型类型参数的属性将导致编译时错误:
//[CustomAttribute(info = typeof(GenericClass3<int, T, string>))] //Error class ClassD<T> { }
不能从 Attribute 继承泛型类型:
//public class CustomAtt<T> : System.Attribute {} //Error
若要在运行时获得有关泛型类型或类型参数的信息,可以使用 System.Reflection 的方法。有关更多信息,请参见泛型和反射(C# 编程指南)
泛型类型中的变化(C# 编程指南)
在 C# 中添加泛型的一个主要好处是能够使用 System.Collections.Generic 命名空间中的类型,轻松地创建强类型集合。例如,您可以创建一个类型为 List<int> 的变量,编译器将检查对该变量的所有访问,确保只有 ints 添加到该集合中。与 C# 1.0 版中的非类型化集合相比,这是可用性方面的一个很大改进。
遗憾的是强类型集合有自身的缺陷。例如,假设您有一个强类型 List<object>,您想将 List<int> 中的所有元素追加到 List<object> 中。您可能希望编写如下代码:
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,您可以声明一个简单的帮助器方法来为您执行转换。例如,您可以编写如下方法:
// 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); } }
它使您能够完成以下操作:
// does compile VarianceWorkaround.Add<int, object>(ints, objects);
此示例演示了一种简单的变化解决方法的一些特征。帮助器方法带两个类型参数,分别对应于源和目标,源类型参数 S 有一个约束,即目标类型参数 D。这意味着读取的 List<> 所包含的元素必须可以转换为插入的 List<> 类型的元素。这使编译器可以强制 int 必须可转换为对象。将类型参数约束为从另一类型参数派生被称为裸类型参数约束。
定义一个方法来解决变化问题不算是一种过于拙劣的方法。遗憾的是变化问题很快就会变得非常复杂。下一级别的复杂性产生在当您想要将一个实例化的接口当作另一个实例化的接口时。例如,您有一个 IEnumerable<int>,您想将它传递给一个只以 IEnumerable<object> 为参数的方法。同样,这样做也是有一定意义的,因为您可以将 IEnumerable<object> 看作对象的序列,将 IEnumerable<int> 看作 ints 的序列。由于 ints 是对象,因此 ints 的序列应当可以被当作对象序列。例如:
static void PrintObjects(IEnumerable<object> objects) { foreach (object o in objects) { Console.WriteLine(o); } }
您要调用的方法如下所示:
// would like to do this, but cant ... // ... ints is not an IEnumerable<object> //PrintObjects(ints);
接口 case 的解决方法是:创建为接口的每个成员执行转换的包装对象。它看起来应如下所示:
// 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 {
它使您能够完成以下操作:
PrintObjects(VarianceWorkaround.Convert<int, object>(ints));
同样,请注意包装类和帮助器方法的裸类型参数约束。此系统已经变得相当复杂,但是包装类中的代码非常简单;它只委托给所包装接口的成员,除了简单的类型转换外,不执行其他任何操作。为什么不让编译器允许从 IEnumerable<int> 直接转换为 IEnumerable<object> 呢?
尽管在查看集合的只读视图的情况下,变化是类型安全的,然而在同时涉及读写操作的情况下,变化不是类型安全的。例如,不能用此自动方法处理 IList<> 接口。您仍然可以编写一个帮助器,用类型安全的方式包装 IList<> 上的所有读操作,但是写操作的包装就不能如此简单了。
下面是处理 IList<T> 接口的变化的包装的一部分,它显示在读和写两个方向上的变化所引起的问题。
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[]。例如:
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# 的正确选择。
下面是上述示例的完整源代码。
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(); } } }