程序= 算法 + 数据。编程人员设计好一种算法,例如排序、比较、交换等等。这些算法应该应用于不同的数据类型,而不是为每个数据类型都写一个专有的算法。
CLR 允许创建:泛型引用类型,泛型值类型,泛型接口,泛型委托。
CLR 不允许创建:泛型枚举类型。
在FCL 中泛型最明显的应用就是集合类,FLC 在 System.Collections.Generic 和 System.Collections.ObjectModel 命名空间中提供了多个泛型集合类。System.Collections.Concurrent 命名空间则提供了线程安全的泛型集合类。
开放类型和封闭类型
具有泛型类型参数的类型任然是类型,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) {CHAPTER 12 Generics 273 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; } }
编译并允许上述代码得到下面的结果:
Cannot create an instance of System.Collections.Generic.
Dictionary`2[TKey,TValue] because Type.ContainsGenericParameters is true.
Cannot create an instance of DictionaryStringKey`1[TValue] because
Type.ContainsGenericParameters is true.
Created instance of DictionaryStringKey`1[System.Guid]
Object type=DictionaryStringKey`1[System.Guid]
泛型类型和继承
泛型类型任然是类型,所以能从其他任何类型派生。使用泛型类型并指定类型参数时,实际是在CLR 中定义了一个新的类型对象,这个新的类型对象从泛型类型派生自的哪个类型派生。例如: List<T> 从 Object 派生,所以List<String> 和 List<Giud> 也从Object 派生。指定类型实参不影响继承层次结构,理解这一点,有助于你判断哪些强制类型转换是允许的,哪些不允许。
泛型类型同一性
同一性就是为了方便使用泛型类型,可以像C++中的宏定义那样将一个泛型类型用其他的符号代表。C# 允许使用简化的语法来引用泛型封闭类型,同时不会影响类型的相等性。这个语法要求在源文件顶部使用传统的using 指令,例如:
using DateTimeList = System.Collections.Generic.List<System.DateTime>;
执行下面的代码验证一下:
Boolean sameType = (typeof(List<DateTime>) == typeof(DateTimeList));
代码爆炸
使用泛型类型参数的方法在进行JIT 编译时,CLR 获取方法的IL,用指定的类型实参替换,然后创建恰当的本机代码(这些代码是为操作指定数据类型“量身定制的”),这正是泛型的重要特点。但这样做也有一个缺点: CLR 要为每种不同的方法/类型 组合生成本机代码。我们将这个现象称为代码爆炸。它可能会造成应用程序的工作集显著增大,从而损害性能。
CLR 内建了一些措施能缓解代码爆炸。
1、一次编译,重复使用。为特定的类型实参调用了一个方法后,以后再调用相同的类型实参的方法时,CLR只会在第一次编译代码。
2、CLR 认为所有引用类型实参都完全相同,所以代码能够共享。对于任何引用类型的实参,都会调相同的代码。
但是假如某个类型实参是值类型,CLR 就必须专门为哪个值类型生成本机代码。
泛型接口
如果没有泛型接口,每次用非泛型接口(如 IComparable)来操纵值类型都会发生装箱,而且会失去编译时的类型安全性。因此,CLR 提供了对泛型接口的支持。引用类型或值类型可指定类型实参实现泛型接口。也可保持类型实参的未指定状态来实现泛型接口。
泛型委托
CLR 支持泛型委托,目的是保证任何类型的对象都能以类型安全的方式传给回调方法。泛型委托允许值类型实例在传给回调方法时不进行任何装箱。
委托和接口的逆变和协变泛型类型实参
委托的每个泛型类类型参数都可以标记为协变量或逆变量。利用这个功能,可将泛型委托类型的变量转换为相同的委托类型(但泛型参数类型不同)。泛型类型参数可以是以下任何一种形式。
-
不变量,意味着泛型类型参数不能改变。
-
逆变量,意味着泛型类型参数可以从一个类更改为它的某个派生类。C# 用in 关键字标识逆变量。
-
协变量,意味着泛型类型参数可以从一个类更改为它的某个基类。C# 用 out 关键字标识协变量。
泛型和其他成员
在C# 中,属性、索引器、事件、操作符方法、构造器和终结器本身不能有类型参数。但它们能在泛型类型中定义,而且这些成员中的代码能使用类型的类型参数。
可验证型和约束
约束的作用是限制能指定成泛型实参的类型数量。通过限制类型的数量,可以对哪些类型执行更多的操作。
编译器/CLR 允许向类型参数应用各种约束。可以用一个主要约束、一个次要约束以及/或者一个构造器约束来约束类型参数。
主要约束
主要约束可以是代表非密封类的一个引用类型。不能指定以下特殊引用类型:SystemObject,SystemArray,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 约束向编译器承诺类型实参是引用类型。任何类类型、接口类型、委托类型或者数组类型都满足这个约束。
struct 约束向编译器承诺类型实参是值类型。包括枚举在内的任何值类型都满足这个约束。但是编译器和 CLR 将任何System.Nullable<T> 值类型视为特殊类型,不满足这个 struct 约束。
次要约束
次要约束代表接口类型。这种约束向编译器承诺类型实参实现了接口。这种约束向编译器承诺类型实参实现了接口。由于能指定多个接口约束,所以类型实参必须实现了所有接口约束。
还有一种次要约束称为 类型参数约束,有时也称为 裸类型约束。它允许一个泛型类型或方法规定:指定的类型实参要么就是约束的类型,要么就是约束的类型的派生类。一个类型参数可以指定零个或者多个类型参数约束。下面这个泛型方法演示了如何使用类型参数约束:
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; }
ConvertList 方法指定了两个类型参数,其中T 参数由Tbase 类型参数约束。意味着不管为T 指定什么类型实参,都必须兼容于为 TBase 指定的类型实参。
构造器约束
类型参数可指定零个或一个构造器约束,它向编译器承诺类型实参是实现了公共无参构造器的非抽象类型。以下示例类使用构造器约束来约束它的类型参数:
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(); } }
在上述例子中 new T() 是合法的,因为已知 T 是拥有公共无参构造器的类型。对所有值类型来说,这一点(拥有公共无参构造器)肯定成立。对于作为类型实参指定的任何引用类型,这一点也成立,因为构造器约束要求它必须成立。
其他可验证性问题
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 }
2、将泛型类型变量设为 null 是非法的,除非将泛型类型约束成引用类型。
3、无论泛型类型是否被约束,使用== 或 != 操作符将泛型类型变量与 null 进行比较都是合法的。如果T 被约束成 struct , C# 编译器会报错。值类型的变量不能与null 进行比较,因为结果始终一样。
4、如果泛型类型参数不能肯定是引用类型,对同一个泛型类型的两个变量进行比较是非法的:
private static void ComparingTwoGenericTypeVariables<T>(T o1, T o2) { if (o1 == o2) { } // Error }
5、将操作符用于泛型类型的操作会引发大量问题。C# 知道如何解释应用于基元类型的操作符(比如+,- ,* 和/)。但是不能将这些操作符用于泛型类型的变量。编译器在编译时确定不了类型。