CLR via C#, 4th -- 【设计类型】 -- 第12章泛 型
泛型(generic)是CLR和编程语言提供的一种特殊机制,它支持另一种形式的代码重用,即“算法重用”。
简单地说,开发人员先定义好算法,比如排序、搜索、交换、比较或者转换等。但是,定义算法的开发人员并不设定该算法要操作什么数据类型;该算法可广泛地应用于不同类型的对象。然后,另一个开发人员只要指定了算法要操作的具体数据类型,就可以开始使用这个算法了。
大多数算法都封装在一个类型中,CLR允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型。此外,CLR还允许创建泛型接口和泛型委托。方法偶尔也封装有用的算法,所以CLR允许在引用类型、值类型或接口中定义泛型方法。
类型参数(type arameter)
定义泛型类型或方法时,为类型指定的任何变量(比如T)都称为类型参数(type arameter),T是变量名,源代码能使用数据类型的任何地方都能使用T。
注意 根据Microsoft的设计原则,泛型参数变量要么称为T,要么至少以大写T开头(如TKey和TValue),大写T代表类型(Type),就像大写1代表接口(Interface)一样,比如IComparable.
类型实参(type argument)
使用泛型类型或方法时指定的具体数据类型称为类型实参(type argument)
private static void SomeMethod() { // Construct a List that operates on DateTime objects List<DateTime> dtList = new List<DateTime>(); // Add a DateTime object to the list dtList.Add(DateTime.Now); // No boxing // Add another DateTime object to the list dtList.Add(DateTime.MinValue); // No boxing // Attempt to add a String object to the list dtList.Add("1/1/2004"); // Compiletime error // Extract a DateTime object out of the list DateTime dt = dtList[0]; // No cast required }
泛型为开发人员提供了以下优势
源代码保护
使用泛型算法的开发人员不需要访问算法的源代码。然而,使用C++模板的泛型技术时,算法的源代码必须提供给准备使用算法的用户。
类型安全
将泛型算法应用于一个具体的类型时,编译器和CLR能理解开发人员的意图,并保证只有与指定数据类型兼容的对象才能用于算法。试图使用不兼容类型的对象会造成编译时错误,或在运行时抛出异常。
更清晰的代码
由于编译器强制类型安全性,所以减少了源代码中必须进行的强制类型转换次数,使代码更容易编写和维护。
更佳的性能
没有泛型的时候,要想定义常规化的算法,它的所有成员都要定义成操作Object数据类型。要用这个算法来操作值类型的实例,CLR必须在调用算法的成员之前对值类型实例进行装箱。正如第5章“基元类型、引用类型和值类型”讨论的那样,装箱造成在托管堆上进行内存分配,造成更频繁的垃圾回收,从而损害应用程序的性能。由于现在能创建一个泛型算法来操作一种具体的值类型,所以值类型的实例能以传值方式传递,CLR不再需要执行任何装箱操作。此外,由于不再需要进行强制类型转换(参见上一条),所以CLR无需验证这种转型是否类型安全,这同样提高了代码的运行速度。
12.1 FCL中的泛型
泛型最明显的应用就是集合类。FCL在System.Collections.Generic和System.Collections.ObjectModel命名空间中提供了多个泛型集合类。System.Collections.Concurrent命名空间则提供了线程安全的泛型集合类。
Microsoft建议使用泛型集合类,不建议使用非泛型集合类。
- 首先,使用非泛型集合类,无法像使用泛型集合类那样获得类型安全性、更清晰的代码以及更佳的性能。
- 其次,泛型类具有比非泛型类更好的对象模型。例如,虚方法数量显著变少,性能更好。
- 另外,泛型集合类增添了一些新成员,为开发人员提供了新的功能。
12.2 泛型基础结构
为了使泛型能够工作,Microsoft必须完成以下工作。
- 创建新的1L指令,使之能够识别类型实参。
- 修改现有元数据表的格式,以便表示具有泛型参数的类型名称和方法。
- 修改各种编程语言(CH,Microsoft Visual Basic.NET等)来支持新语法,允许开发人员定义和引用泛型类型和方法。
- 修改编译器,使之能生成新的IL指令和修改的元数据格式。
- 修改JIT编译器,以便处理新的支持类型实参的IL指令来生成正确的本机代码。
- 创建新的反射成员,使开发人员能查询类型和成员,以判断它们是否具有泛型参数。另外,还必须定义新的反射成员,使开发人员能在运行时创建泛型类型和方法定义。
- 修改调试器以显示和操纵泛型类型、成员、字段以及局部变量
- 修改Microsoft Visual Studio的“智能感知”(Intellisense)功能。将泛型类型或方法应用于特定数据类型时能显示成员的原型。
12.2.1 开放类型和封闭类型
开放类型
具有泛型类型参数的类型仍然是类型,CLR同样会为它创建内部的类型对象。这一点适合引用类型(类)、值类型(结构)、接口类型和委托类型。然而,具有泛型类型参数的类型称为开放类型,CLR禁止构造开放类型的任何实例。这类似于CLR禁止构造接口类型的实例。
封闭类型
代码引用泛型类型时可指定一组泛型类型实参。为所有类型参数都传递了实际的数据类型.类型就成为封闭类型。CLR允许构造封闭类型的实例。然而,代码引用泛型类型的时候,可能留下一些泛型类型实参未指定。这会在CLR中创建新的开放类型对象,而且不能创建该类型的实例。
using System; using System.Collections.Generic; // A partially specified open type internal sealed class DictionaryStringKey<TValue> : Dictionary<String, TValue> { } public static class Program { public static void Main() { Object o = null; // Dictionary<,> is an open type having 2 type parameters Type t = typeof(Dictionary<,>); // Try to create an instance of this type (fails) o = CreateInstance(t); Console.WriteLine(); // DictionaryStringKey<> is an open type having 1 type parameter t = typeof(DictionaryStringKey<>); // Try to create an instance of this type (fails) o = CreateInstance(t); Console.WriteLine(); // DictionaryStringKey<Guid> is a closed type t = typeof(DictionaryStringKey<Guid>); // Try to create an instance of this type (succeeds) o = CreateInstance(t); // Prove it actually worked Console.WriteLine("Object type=" + o.GetType()); } private static Object CreateInstance(Type t) { Object o = null; try { o = Activator.CreateInstance(t); Console.Write("Created instance of {0}", t.ToString()); } catch (ArgumentException e) { Console.WriteLine(e.Message); } return o; } }
还要注意,CLR会在类型对象内部分配类型的静态字段(本书第4章“类型基础”对此进行了讨论)。因此,每个封闭类型都有自己的静态字段。换言之,假如List<D定义了任何静态字段,这些字段不会在一个List<DateTime>和一个List<String>之间共享:每个封闭类型对象都有自己的静态字段。另外,假如泛型类型定义了静态构造器(参见第8章“方法”),那么针对每个封闭类型,这个构造器都会执行一次。泛型类型定义静态构造器的目的是保证传递的类型实参满足特定条件。
12.2.2泛型类型和继承
泛型类型仍然是类型,所以能从其他任何类型派生。使用泛型类型并指定类型实参时,实际是在CLR中定义一个新的类型对象,新的类型对象从泛型类型派生自的那个类型派生。换言之,由于List<T>从Object派生,所以List<String>和List<Guid>也从Object派生。类似地,由于DictionaryStringKey<TValue>从Dictionary<String,TValue>派生,所以DictionaryStringKey<Guid>也从Dictionary<String,Guid>派生。指定类型实参不影响继承层次结构-理解这一点,有助于你判断哪些强制类型转换是允许的,哪些不允许。
internal sealed class Node<T> { public T m_data; public Node<T> m_next; public Node(T data) : this(data, null) { } public Node(T data, Node<T> next) { m_data = data; m_next = next; } public override String ToString() { return m_data.ToString() + ((m_next != null) ? m_next.ToString() : String.Empty); } }
所以,更好的办法是定义非泛型Node基类,再定义泛型TypedNode类(用Node类作为基类)。这样就可以创建一个链表,其中每个节点都可以是一种具体的数据类型(不能是Object),同时获得编译时的类型安全性,并防止值类型装箱。
internal class Node { protected Node m_next; public Node(Node next) { m_next = next; } } internal sealed class TypedNode<T> : Node { public T m_data; public TypedNode(T data) : this(data, null) { } public TypedNode(T data, Node next) : base(next) { m_data = data; } public override String ToString() { return m_data.ToString() + ((m_next != null) ? m_next.ToString() : String.Empty); } }
12.2.3泛型类型同一性
绝对不要单纯出于增强源码可读性的目的来定义一个新类。这样会丧失类型同一性(dentity)和相等性(equivalence)
List<DateTime> dtl = new List<DateTime>(); internal sealed class DateTimeList : List<DateTime> { // No need to put any code in here! } DateTimeList dtl = new DateTimeList(); Boolean sameType = (typeof(List<DateTime>) == typeof(DateTimeList));
C#允许使用简化的语法来引用泛型封闭类型,同时不会影响类型的相等性。这个语法要求在源文件顶部使用传统的using指令,
using DateTimeList = System.Collections.Generic.List<System.DateTime>; Boolean sameType = (typeof(List<DateTime>) == typeof(DateTimeList));
12.2.4 代码爆炸
CLR要为每种不同的方法/类型组合生成本机代码。我们将这个现象称为代码爆炸。
CLR内建了一些优化措施能缓解代码爆炸。首先,假如为特定的类型实参调用了一个方法,以后再用相同的类型实参调用这个方法,CLR只会为这个方法/类型组合编译一次代码。所以,如果一个程序集使用List<DateTime>,一个完全不同的程序集(加载到同一个AppDomain中)也使用List<DateTime>,CLR只为List<DateTime编译一次方法。这样就显著缓解了代码爆炸。
CLR还有另一个优化,它认为所有引用类型实参都完全相同,所以代码能够共享。例如,CLR为List<String>的方法编译的代码可直接用于List<Stream>的方法,因为String和Stream均为引用类型。事实上,对于任何引用类型,都会使用相同的代码。CLR之所以能执行这个优化,是因为所有引用类型的实参或变量实际只是指向堆上对象的指针(32位Windows系统上是32位指针;64位Windows系统上是64位指针),而所有对象指针都以相同方式操纵。
但是,假如某个类型实参是值类型,CLR就必须专门为那个值类型生成本机代码。这是因为值类型的大小不定。即使两个值类型大小一样(比如Int32和UInt32,两者都是32位),CLR仍然无法共享代码,因为可能要用不同的本机CPU指令来操纵这些值。
12.3泛型接口
泛型的主要作用就是定义泛型的引用类型和值类型。然而,对泛型接口的支持对CLR来说也很重要。
public interface IEnumerator<T> : IDisposable, IEnumerator { T Current { get; } } internal sealed class Triangle : IEnumerator<Point> { private Point[] m_vertices; // IEnumerator<Point>'s Current property is of type Point public Point Current { get { ... } } } internal sealed class ArrayEnumerator<T> : IEnumerator<T> { private T[] m_array; // IEnumerator<T>'s Current property is of type T public T Current { get { ... } } }
12.4泛型委托
CLR支持泛型委托,目的是保证任何类型的对象都能以类型安全的方式传给回调方法。此外,泛型委托允许值类型实例在传给回调方法时不进行任何装箱。
public delegate TReturn CallMe<TReturn, TKey, TValue>(TKey key, TValue value);
编译器会将它转换成如下所示的类:
public sealed class CallMe<TReturn, TKey, TValue> : MulticastDelegate { public CallMe(Object object, IntPtr method); public virtual TReturn Invoke(TKey key, TValue value); public virtual IAsyncResult BeginInvoke(TKey key, TValue value, AsyncCallback callback, Object object); public virtual TReturn EndInvoke(IAsyncResult result); }
12.5 委托和接口的逆变和协变泛型类型实参
委托的每个泛型类型参数都可标记为协变量或逆变量。利用这个功能,可将泛型委托类型的变量转换为相同的委托类型(但泛型参数类型不同)。
- 不变量(invariant)意味着泛型类型参数不能更改。到目前为止,你在本章看到的全是不变量形式的泛型类型参数。
- 逆变量(contravariant)意味着泛型类型参数可以从一个类更改为它的某个派生类。在C#是用in关键字标记逆变量形式的泛型类型参数。逆变量泛型类型参数只出现在输入位置,比如作为方法的参数。
- 协变量(covariant)意味着泛型类型参数可以从一个类更改为它的某个基类。C#是用out关键字标记协变量形式的泛型类型参数。协变量泛型类型参数只能出现在输出位置,比如作为方法的返回类型。
covariant=协变量,contravariant逆变量:covariance=协变性,contravariance=逆变性。另外,variance=可变性。简而言之,协变性指定返回类型的兼容性,而逆变性指定参数的兼容性。
public delegate TResult Func<in T, out TResult>(T arg); Func<Object, ArgumentException> fn1 = null; Func<String, Exception>fn2 = fn1; // No explicit cast is required here Exception e = fn2("");
fnl变量引用一个方法,获取一个Object,返回一个ArgumentException。而fn2变量引用另一个方法,获取一个String,返回一个Exception由于可将一个String传给期待Object的方法(因为String从Object派生),而且由于可以获取返回ArgumentException的一个方法的结果,并将这个结果当成一个Exception(因为Exception是ArgumentException的基类),所以上述代码能正常编译,而且编译时能维持类型安全性。
使用要获取泛型参数和返回值的委托时,建议尽量为逆变性和协变性指定in和out关键字。这样做不会有不良反应,并使你的委托能在更多的情形中使用。
和委托相似,具有泛型类型参数的接口也可将类型参数标记为逆变量和协变量。
public interface IEnumerator<out T> : IEnumerator { Boolean MoveNext(); T Current { get; } }
//This method accepts an IEnumerable of any reference type Int32 Count(IEnumerable<Object> collection) {..} //The cal1 below passes an IEnumerable<String> to Count Int32 c = Count(new[] { "Grant"});
为什么必须显式用in或out标记泛型类型参数?
编译器应该能检查委托或接口声明,并自动检测哪些泛型类型参数能够逆变和协变。虽然编译器确实能,但C#团队认为必须由你订立协定(contract),明确说明想允许什么。例如,假定编译器判断一个泛型类型参数是逆变量(用在输入位置),但你将来向某个接口添加了成员,并将类型参数用在了输出位置。下次编译时,编译器将认为该类型参数是不变量。但在引用了其他成员的所有地方,只要还以为“类型参数是逆变量”就可能出错。
因此,编译器团队决定,在声明泛型类型参数时,必须由你显式使用in或out来标记可变性。以后使用这个类型参数时,假如用法与声明时指定的不符,编译器就会报错,提醒你违反了自己订立的协定。如果为泛型类型参数添加in或out来打破原来的协定,就必须修改使用旧协定的代码。
12.6 泛型方法
定义泛型类、结构或接口时,类型中定义的任何方法都可引用类型指定的类型参数。类型参数可作为方法参数、方法返回值或方法内部定义的局部变量的类型使用。然而,CLR还允许方法指定它自己的类型参数。这些类型参数也可作为参数、返回值或局部变量的类型使用。
internal sealed class GenericType<T> { private T m_value; public GenericType(T value) { m_value = value; } public TOutput Converter<TOutput>() { TOutput result = (TOutput) Convert.ChangeType(m_value, typeof(TOutput)); return result; } }
泛型方法和类型推断
为了改进代码的创建,增强可读性和可维护性,C#编译器支持在调用泛型方法时进行类型推断。这意味着编译器会在调用泛型方法时自动判断(或者说推断)要使用的类型。
private static void CallingSwapUsingInference() { Int32 n1 = 1, n2 = 2; Swap(ref n1, ref n2);// Calls Swap<Int32> String s1 = "Aidan"; Object s2 = "Grant"; Swap(ref s1, ref s2);// Error, type can't be inferred }
推断类型时,C#使用变量的数据类型,而不是变量引用的对象的实际类型。
类型可定义多个方法,让其中一个方法接受具体数据类型,让另一个接受泛型类型参数。
private static void Display(String s) { Console.WriteLine(s); } private static void Display<T>(T o) { Display(o.ToString()); // Calls Display(String) }
12.7 泛型和其他成员
在C#中,属性、索引器、事件、操作符方法、构造器和终结器本身不能有类型参数。但它们能在泛型类型中定义,而且这些成员中的代码能使用类型的类型参数。
12.8 可验证性和约束
private static Boolean MethodTakingAnyType<T>(T o) { T temp = o; Console.WriteLine(o.ToString()); Boolean b = temp.Equals(o); return b; }
无论T是引用类型,是值类型或枚举类型,还是接口或委托类型,它都能工作。这个方法适用于当前存在的所有类型,也适用于将来可能定义的任何类型,因为所有类型都支持对Object类型的变量的赋值,也支持对Object类型定义的方法的调用(比如ToString和Equals)
private static T Min<T>(T o1, T o2) { if (o1.CompareTo(o2) < 0) return o1; return o2; }
Min方法试图使用o1变量来调用CompareTo方法。但是,许多类型都没有提供CompareTo方法,所以C#编译器不能编译上述代码,它不能保证这个方法适用于所有类型。
约束的作用是限制能指定成泛型实参的类型数量。通过限制类型的数量,可以对那些类型执行更多操作。
public static T Min<T>(T o1, T o2) where T : IComparable<T> { if (o1.CompareTo(o2) < 0) return o1; return o2; }
CLR不允许基于类型参数名称或约束来进行重载:只能基于元数(类型参数个数)对类型或方法进行重载。
// It is OK to define the following types: internal sealed class AType {} internal sealed class AType<T> {} internal sealed class AType<T1, T2> {} // Error: conflicts with AType<T> that has no constraints internal sealed class AType<T> where T : IComparable<T> {} // Error: conflicts with AType<T1, T2> internal sealed class AType<T3, T4> {} internal sealed class AnotherType { // It is OK to define the following methods: private static void M() {} private static void M<T>() {} private static void M<T1, T2>() {} // Error: conflicts with M<T> that has no constraints private static void M<T>() where T : IComparable<T> {} // Error: conflicts with M<T1, T2> private static void M<T3, T4>() {} }
重写虚泛型方法时,重写的方法必须指定相同数量的类型参数,而且这些类型参数会继承在基类方法上指定的约束。事实上,根本不允许为重写方法的类型参数指定任何约束。但类型参数的名称是可以改变的。类似地,实现接口方法时,方法必须指定与接口方法等量的类型参数,这些类型参数将继承由接口方法指定的约束。
internal class Base { public virtual void M<T1, T2>() where T1 : struct where T2 : class { } } internal sealed class Derived : Base { public override void M<T3, T4>() where T3 : EventArgs // Error where T4 : class // Error { } }
12.8.1 主要约束
类型参数可以指定零个或者一个主要约束。主要约束可以是代表非密封类的一个引用类型。
不能指定以下特殊引用类型:System.Object,System.Array,System.Delegate,System.MulticastDelegate,System.ValueType,System.Enum或者System.Void.
internal sealed class PrimaryConstraintOfStream<T> where T : Stream { public void M(T stream) { stream.Close();// OK } }
有两个特殊的主要约束:class和struct。其中,class约束向编译器承诺类型实参是引用类型。任何类类型、接口类型、委托类型或者数组类型都满足这个约束。
internal sealed class PrimaryConstraintOfClass<T> where T : class { public void M() { T temp = null;// Allowed because T must be a reference type } }
struct约束向编译器承诺类型实参是值类型。包括枚举在内的任何值类型都满足这个约束。但编译器和CLR将任何System.Nullable<>值类型视为特殊类型,不满足这个struct约束。原因是Nullable<T>类型将它的类型参数约束为struct,而CLR希望禁止像Nullable<Nullable<T>>这样的递归类型。
internal sealed class PrimaryConstraintOfStruct<T> where T : struct { public static T Factory() { // Allowed because all value types implicitly // have a public, parameterless constructor return new T(); } }
12.8.2 次要约束
类型参数可以指定零个或者多个次要约束,次要约束代表接口类型。
还有一种次要约束称为类型参数约束,有时也称为裸类型约束。这种约束用得比接口约束少得多。它允许一个泛型类型或方法规定:指定的类型实参要么就是约束的类型,要么是约束的类型的派生类。一个类型参数可以指定零个或者多个类型参数约束。
private static List<TBase> ConvertIList<T, TBase>(IList<T> list) where T : TBase { List<TBase> baseList = new List<TBase>(list.Count); for (Int32 index = 0; index < list.Count; index++) { baseList.Add(list[index]); } return baseList; }
12.8.3构造器约束
类型参数可指定零个或一个构造器约束,它向编译器承诺类型实参是实现了公共无参构造器的非抽象类型。
internal sealed class ConstructorConstraint<T> where T : new() { public static T Factory() { // Allowed because all value types implicitly // have a public, parameterless constructor and because // the constraint requires that any specified reference // type also have a public, parameterless constructor return new T(); } }
12.8.4 其他可验证性问题
1,泛型类型变量的转型
将泛型类型的变量转型为其他类型是非法的,除非转型为与约束兼容的类型
private static void CastingAGenericTypeVariable1<T>(T obj) { Int32 x = (Int32) obj; // Error String s = (String) obj; // Error } private static void CastingAGenericTypeVariable2<T>(T obj) { Int32 x = (Int32) (Object) obj; // No error String s = (String) (Object) obj; // No error }
转型为引用类型时还可使用C#as操作符。
private static void CastingAGenericTypeVariable3<T>(T obj) { String s = obj as String; // No error }
2,将泛型类型变量设为默认值
将泛型类型变量设为null是非法的,除非将泛型类型约束成引用类型。
private static void SettingAGenericTypeVariableToNull<T>() { T temp = null; // CS0403 – Cannot convert null to type parameter 'T' because it could // be a nonnullable value type. Consider using 'default(T)' instead }
Microsoft的C#团队认为有必要允许开发人员将变量设为它的默认值,并专门为此提供了default关键字
private static void SettingAGenericTypeVariableToDefaultValue<T>() { T temp = default(T); // OK }
default关键字告诉C#编译器和CLR的JIT编译器,如果T是引用类型,就将temp设为null;如果是值类型,就将temp的所有位设为0。
3,将泛型类型变量与null进行比较
无论泛型类型是否被约束,使用==或!=操作符将泛型类型变量与null进行比较都是合法的
private static void ComparingAGenericTypeVariableWithNull<T>(T obj) { if (obj == null) { /* Never executes for a value type */ } }
由于T未进行约束,所以可能是引用类型或值类型。如果T是值类型,obj永远都不会为null.你或许以为C#编译器会报错。但C#编译器并不报错;相反,它能顺利地编译代码。调用这个方法时,如果为类型参数传递值类型,那么JT编译器知道if语句永远都不会为true,所以不会为if测试或者大括号内的代码生成本机代码。如果换用!-操作符,JT编译器不会为if测试生成代码(因为它肯定为true),但会为i大括号内的代码生成本机代码。
顺便说一句,如果T被约束成struct,C#编译器会报错。值类型的变量不能与null进行比较,因为结果始终一样。
4,两个泛型类型变量相互比较
如果泛型类型参数不能肯定是引用类型,对同一个泛型类型的两个变量进行比较是非法的
private static void ComparingTwoGenericTypeVariables<T>(T o1, T o2) { if (o1 == o2) { } // Error }
在这个例子中,T未进行约束。虽然两个引用类型的变量相互比较是合法的,但两个值类型的变量相互比较是非法的,除非值类型重载了==操作符。如果T被约束成class,上述代码能通过编译。如果变量引用同一个对象,==操作符会返回true。注意,如果T被约束成引用类型,而且该引用类型重载了operator==方法,那么编译器会在看到==操作符时生成对这个方法的调用。显然,所有些讨论也适合!=操作符。
不允许将类型参数约束成具体的值类型,因为值类型隐式密封,不可能存在从值类型派生的类型。如果允许将类型参数约束成具体的值类型,那么泛型方法会被约束为只支持该具体类型,这还不如不要泛型呢!
5,泛型类型变量作为操作数使用
不能将这些操作符应用于泛型类型的变量。编译器在编译时确定不了类型,所以不能向泛型类型的变量应用任何操作符。
这是CLR的泛型支持体系的一个严重限制,许多开发人员(尤其是科学、金融和数学领域的开发人员)对这个限制感到很失望。
浙公网安备 33010602011771号