泛型(generic)是CLR和编程语言提供一种特殊机制,它支持另一种形式的代码重用,即"算法重用"。
简单地说,开发人员先定义好一个算法,比如排序、搜索、交换等。但是定义算法的开发人员并不设定该算法要操作什么数据类型;该算法可广泛地应用于不同类型的对象。然后,另一个开发人员只要指定了算法要操作的具体数据类型,就可以使用这个现成的算法了。
泛型有两种表现形式:泛型类型和泛型方法。
泛型类型:大多数算法都封装在一个类型中,CLR允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型。除此之外,CLR还允许创建泛型接口和泛型委托。
泛型方法:方法偶尔也封装有用的算法,所以CLR允许引用类型、值类型或接口中定义泛型方法。
两者都是表示API的基本方法(不管是指一个泛型方法还是一个完整的泛型类型),以致平时期望出现一个普通类型的地方出现一个类型参数。比如,List
类型参数是真实类型的占位符。在泛型声明中,类型参数要放在一堆尖括号内,并以逗号分隔。所以,在Dictionary
泛型为开发人员提供了以下优势:
1)源代码保护 使用一个泛型算法的开发人员不需要访问算法的源代码。然而,使用C++模板的泛型技术时,算法的源代码必须提供给准备使用算法的用户。
2)类型安全 将一个泛型算法应用于一个具体的类型时,编译器和CLR能理解开发人员的意图,并保证只有与制定数据类型兼容的对象才能随同算法使用。
3)更清晰的代码 由于编译器强制类型安全性,所以减少了源代码中必须进行的转型次数。
4)更佳的性能 在有泛型之前,要想定义一个常规化的算法,它的所有成员都要定义成操作Object数据类型。这其中就要有装箱和拆箱之间的性能损失。由于现在能创建一个泛型算法来操作一个具体的值类型,所以值类型的实例能以传值的方式传递,CLR不再需要只需任何装箱操作。由于不再需要转型,所以CLR不必检查尝试一次转型操作是否类型安全,同样提高了代码的允许速度。
一、 Framework类库中的泛型
二、Wintellect的Power Collections库
Power Collections库由Wintellect制作,这个库有一系列集合类构成,任何人都可以免费下载和使用。
集合类名称 | 说明 |
BigList |
有序T对象集合。操作100个以上的数据项是,效率非常高 |
Bag |
无序T对象的集合,集合进行了哈希处理,并允许重复项 |
OrderedBag |
有序T对象的集合,允许重复值 |
Set |
无序T数据项集合,不允许重复项。添加重复项后,会只保留一个 |
OrderedSet |
有序T数据项的集合,不允许重复项 |
Deque |
双端队列(double-ending queue)。类似于一个列表,但在起始处添加/删除数据项时,比列表更高效 |
OrderedDictionary |
字典,其中的键进行了排序,每个键都有一个对应的值 |
MultiDictionary |
字典,其中每个键都可以有多个值,对键进行了哈希处理,允许重复,而且数据项是无序的 |
OrderedMultiDictionary |
字典,其中的键进行了排序,每个键都可以有多个值(同样进行了排序)。允许重复的键 |
三、泛型的基础结构
为了是泛型能够工作,Microsoft必须完成以下工作:
internal static class Program { private static void Main(string[] args) { Object o = null; // Dictionary<,> 是一个开放类型,有两个类型参数 Type t = typeof(Dictionary<,>); // 尝试创建该类型的一个实例 (失败) o = CreateInstance(t); Console.WriteLine(); // DictionaryStringKey<> 是一个开放类型,有一个类型参数 t = typeof(DictionaryStringKey<>); // 尝试创建该类型的一个实例 (失败) o = CreateInstance(t); Console.WriteLine(); // DictionaryStringKey是一个封闭类型 t = typeof(DictionaryStringKey); // 尝试创建该类型的一个实例 (成功) o = CreateInstance(t); // Prove it actually worked Console.WriteLine("Object type=" + o.GetType()); Console.ReadKey(); } private static Object CreateInstance(Type t) { Object o = null; try { o = Activator.CreateInstance(t); Console.Write("已创建 {0} 的实例", t.ToString()); } catch (ArgumentException e) { Console.WriteLine(e.Message); } return o; } // A partially specified open type internal sealed class DictionaryStringKey : Dictionary { } }
最后显示地结果为:
internal sealed calss GenericTypeThatReqiresAnEnum{ static GenericTypeThatReqiresAnEnum() { if ( !typeof (T).IsEnum) { throw new ArgumentException("T must be an enumerated type") } } }
CLR提供了一个名为"约束"(constraint)的功能,可利用它更好地定义一个泛型类型来指出哪个类型实参是有效的。
Listdt = new List ();
一些开发人员可能首先定义下面这样的一个类:
internal sealed class DateTimeList : List{ //这里无需放任何代码! }
然后就可以进一步简化创建:
DateTimeList dt = new DateTimeList ();
这样做表面上是方便了,但是决定不要单纯处于增强源代码的易读性类这样定义一个新类。这样会丧失类型同一性(identity)和相等性(equivalence)。如下:
Boolean sameType = (typeof(List) == (typeof(DateTimeList));
上述代码运行时,sameType会初始化为false,因为比较的是两个不同类型的对象。也就是说,假如一个方法的原型接受一个DateTimeList,那么不能将一个List
using DateTimeList = System.Collections.Generic.List;
现在只想下面这行代码时,sameType会初始化为true:
Boolean sameType = (type(List) == (ypeof(DateTimeList));
还有,可以使用C#的隐式类型局部变量功能,让编译器根据表达式的类型来推断一个方法的局部变量的类型。
public interface IEnumerator: IDisposable, IEnumerator{ T Current { get; } }
下面的示例类型实现上述泛型接口,而且指定了类型实参。
internal sealed class Triangle : IEnumerator{ private Point[] m_Vertice; public Point Current { get { ... } } }
下面实现了相同的泛型接口,但保持类型实参的未指定状态:
internal sealed class ArrayEnumerator: IEnumerator { private T[] m_Vertice; public TCurrent { get { ... } } }
五、泛型委托
CLR支持泛型委托,目的是保证任何类型的对象都能以一种类型安全的方式传给一个回调方法。
此外,泛型委托允许一个值类型的实例在传给一个回调方法时不执行任何装箱操作。
public delegate TReturn CallMe(TKey key, TValue value);
编译器会将它转化成一个类,该类在逻辑上可以这样表示:
public sealed class CallMe: MulticastDelegate { public CallMe(Object object, IntPtr method); public virtual TReturn Invoke(TKey key, TValue value); public virtual IAsycResult BeginInvoke(TKey key, TValue value, AsyncCallback callback, Object object); public virtual TReturn EndInvoke(IAsycResult result); }
反编译后
建议尽量使用在FVL中预定义的泛型Action和Func委托。
public delegate TResult Func<in T, Out TResult>(T arg);
其中,泛型类型参数T用in关键字标记,这使它成为一个逆变量;泛型类型参数TResulr则用out关键字标记,这是它成为一个协变量。
Func
就可以将它转型为另一个泛型类型参数不同的Func类型:
Funcfn2 = fn1; //不需要显示转型 Exception e = fn("");
使用要获取泛型参数和返回值的委托时,建议尽量为逆变性和协变性指定in和out关键字。这样做不会有不良反应,并使你的委托能在更多的情形中使用。
public interface IEnumerator<out T> : IEnumerator { Boolean MoveNext(); T Current{ get; } }
由于T是逆变量,所以以下代码可以顺利编译:
//这个方法接受任意引用类型的一个IEnumerable Int32 Count(IEnumerable
七、泛型方法
定义泛型类、结构或接口时,这些类型中定义的任何方法都可引用由类型指定的一个类型参数。类型参数可以作为方法的参数,作为方法的返回值,或者作为方法内部定义的一个局部变量来使用。
internal sealed class FenericType{ privete T m_value; public GenericType(T value) { m_value = value; } public TOutput Converter () { TOutput resulr= (TOurput) Convert.ChangeType(m_value,typeof(TOutput)); return result; } }
1.泛型方法和类型推断
private static void CallingSwapUsingInference() { Int32 n1 = 1, n2 = 2; Swap(ref n1, ref n2); //调用SwapString s1 = "A"; Object s2 = "B"; Swap(ref s1, ref s2); //错误,不能推断类型 }
执行类型推断时,C#使用变量的数据类型,而不是由变量引用的对象的实际类型。
八、泛型和其他成员
在C#中,属性、索引器、事件、操作符方法、构造器和终结器(finalizer)本身不能有类型参数。但是,它们能在一个泛型类型中定义,而且这些成员中的代码能使用类型的类型参数。
九、可验证性和约束
public static T Min(T o1, T o2) where T : IComparable { if (o1.CompareTo(o2))<0 return o1; return o2; }
C#的where关键字告诉编译器,为T指定的任何类型都必须实现同类型(T)的泛型IComparable接口。有了这个约束,就可以在方法中调用CompareTo,因为已知IComparable
internal sealed class OverloadingByArity { // 可以定义一下类型 internal sealed class AType { } internal sealed class AType{ } internal sealed class AType { } // 错误: 与没有约束的 AType 起冲突 internal sealed class ATypewhere T : IComparable { } // 错误: 与 AType 起冲突 internal sealed class AType{ } internal sealed class AnotherType { // 可以定义一下方法,参数个数不同: private static void M() { } private static void M () { } private static void M () { } // 错误: 与没有约束的 M 起冲突 private static void M() where T : IComparable { } // 错误: 与 M 起冲突 private static void M() { } } }
internal static class OverridingVirtualGenericMethod { internal class Base { public virtual void M() where T1 : struct where T2 : class { } } internal sealed class Derived : Base { public override void M () /*where T3 : struct where T4 : class */ { } } }
1 主要约束
internal static class PrimaryConstraintOfStreamwhere T : Stream { public static void M(T stream) { stream.Close(); // OK } }
有两个特殊的主要约束:class和struct。其中,class约束是指类型实参是一个引用类型。任何类类型、接口类型、委托类型或者数组类型都满足这个约束。例如:
internal static class PrimaryConstraintOfClasswhere T : class { public static void M() { T temp = null; // 允许,T为引用类型 } }
struct约束向编译器承诺一个指定的类型实参是值类型。包括枚举在内的任何值类型都满足这个约束。然而,编译器和CLR将任何System.Nullable
internal static class PrimaryConstraintOfStructwhere T : struct { public static T Factory() { // 允许,因为值类型都有一个隐式无参构造器 return new T(); } }
internal static class SecondaryConstraints { private static ListConvertIList (IList list) where T : TBase { List baseList = new List (list.Count); for (Int32 index = 0; index < list.Count; index++) { baseList.Add(list[index]); } return baseList; } private static void CallingConvertIList() { //构造并初始化一个List (它实现了IList IList) ls = new List (); ls.Add("A String"); // 将IList 转换成IList IList
internal sealed class ConstructorConstraints { internal sealed class ConstructorConstraintwhere T : new() { public static T Factory() { // 允许,因为值类型都有隐式无参构造器 // 而约束要求任何引用类型也要有一个无参构造器 return new T(); } } }
目前,Microsoft只支持无参构造器约束。
private void CastingAGenericTypeVariable1(T obj) { Int32 x = (Int32)obj; // 错误 String s = (String)obj; // 错误 }
上述两行错误是因为T可以是任何任何类型,无法保证成功。
private void CastingAGenericTypeVariable2(T obj) { Int32 x = (Int32)(Object)obj; // 不报错 String s = (String)(Object)obj; // 不报错 }
现在虽然能编译通过,但运行时也无法保证是正确的。
internal sealed class SettingAGenericTypeVariableToADefaultValue { private void SettingAGenericTypeVariableToNull() { //T temp = null; // 错误, 值类型不能设置为null,可考虑使用default('T') } private void SettingAGenericTypeVariableToDefaultValue () { T temp = default(T); // 正确 } }
private void ComparingAGenericTypeVariableWithNull(T obj) { if (obj == null) { /* 对值类型来说,永远不会执行 */ } }
如果T被约束成一个struct,C#编译器会报错。
private void ComparingTwoGenericTypeVariables(T o1, T o2) { //if (o1 == o2) { } // 错误 }
5)泛型类型变量作为操作书使用
internal static class UsingGenericTypeVariablesAsOperands { private T Sum(T num) where T : struct { T sum = default(T); for (T n = default(T); n < num; n++) sum += n; return sum; } }
上面代码会出很多错误,比如:运算符"<"无法应用于"T"和"T"类型的操作数等。