泛型,是CLR和编程语言提供的一种特殊机制,它支持另一种形式的代码重用,即“算法重用”。
简单的说,开发人员先定义好一个算法,比如排序、搜索、交换、比较或者转换等。但是,定义算法的开发人员并不设改算法要操作什么数据类型:改算法可广发地应用于不同类型的对象。然后,另一个开发人员只要指定了算法要操作的具体数据类型,就可以开始使用这个现成的算法了。例如,可用一个排序算法来操作Int32 和String等类型的对象。
1、大多数算法都封装在一个类型中,CLR允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型。除此之外,CLR还允许创建泛型接口和反省委托。所以,CLR允许在引用类型、值类型或接口中定义泛型方法。
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"); // Compile-time error 编译时错误 // Extract a DateTime object out of the list DateTime dt = dtList[0]; // No cast required 不需要转型 }
2、泛型的优势:
- 源代码保护:使用一个泛型算法的开发人员不需要访问算法的源代码。
- 类型安全:将一个泛型算法应用于一个具体的类型时,保证只有与指定数据类型兼容的对象才能同算法使用。若试图使用不兼容类型的一个对象,会造成编译时错误,或在运行时抛出异常。
- 更加清晰的代码:由于编译器强制类型的安全性,减少了源代码中必须进行的转型次数。
- 更佳的性能:减少不必要的装箱、拆箱;由于不再需要转型,所以CLR不必检查尝试的一次转型操作是否类型安全,提高代码的运行速度。
3、Framework类库中的泛型 泛型最明显的应用就是集合类。FCL已经定义了几个泛型集合类。其中大多数都在System.Collections.Generic和System.Collection.ObjectModel命名空间中。
4、泛型类型和继承: 泛型类型仍然是类型,所以它能从其他任何类型派生。由于List<T>是从Objec派生的,所以List<String> 和List<Guid>也从Object派生。
5、泛型接口:泛型的主要作用就是定义泛型的引用类型和值类型。然而,对泛型接口的支持对CLR来说也很重要。没有泛型接口,每次试图使用一个非泛型接口(如IComparable)来操纵一个值类型,都会发生装箱,而且会失去编译时的类型安全性。
以下泛型接口定义时FCL的一部分(在System.Collections.Generic 命名空间中):
public interface IEnumerator<T> : IDisposable, IEnumerator { T Current { get; } }
下面的示例类型实现了上述泛型接口,而且指定了类型实参。注意,Triangle对象可枚举一组Point对象。还要注意,Current属性具有Point数据类型。
internal sealed class Triangle : IEnumerator<Point> { private Point[] m_vertices; // IEnumerator<Point>'s Current property is of type Point public Point Current { get { ... } } ... }
6、泛型委托: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); }
7、泛型方法:定义泛型类、结构或接口时,这些类型中定义的任何方法都可引用由类型指定的一个类型参数。类型参数可以作为方法的参数,作为方法的返回值,或者作为方法内部定义的一个局部变量来使用。
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; //返回将value转换成TOutput类型之后的结果 } }
在这个例子中,GenericType类定义了自己的类型参数(T),Converter 方法也定义了自己的类型参数(TOutput)。这样的GenericType可以处理任何类型。Convert方法能将m_value字段引用的对象转换成任意类型——具体取决于传给它的类型实参是什么。泛型方法的存在,为开发人员提供了极大的灵活性。
Swap方法:
private static void Swap<T>(ref T o1, ref T o2){ T temp=o1; o1=o2; o2=temp; }
可以在代码中像这样调用Swap:
private static void CallingSwap() { Int32 n1 = 1, n2 = 2; Console.WriteLine("n1={0}, n2={1}", n1, n2);//1,2 Swap<Int32>(ref n1, ref n2); Console.WriteLine("n1={0}, n2={1}", n1, n2);//2,1 String s1 = "Aidan", s2 = "Grant"; Console.WriteLine("s1={0}, s2={1}", s1, s2);//Aidan,Grant Swap<String>(ref s1, ref s2); Console.WriteLine("s1={0}, s2={1}", s1, s2);//Grant,Aidan }
8、其他可验证问题
1)、泛型类型变量的转型:将一个泛型类型变量转型为另一个类型是非法的,除非将其转型为与一个约束兼容的类型。
private static void CastingAGenericTypeVariable1<T>(T obj) { Int32 x = (Int32) obj; // Error 无法将类型”T"转换为"int" String s = (String) obj; // Error 无法将类型“T"转换为"string" }
上述两行会造成编译器报错,因为T可能是任意类型,无法保证能成功转型。为了修改上述代码使其能编译通过,可以先转型为Object:
private static void CastingAGenericTypeVariable2<T>(T obj) { Int32 x = (Int32) (Object) obj; // No error String s = (String) (Object) obj; // No error }
虽然代码现在能够编译,但CLR仍有可能在运行时抛出一个InvalidCastException异常。
如果试图转型为一个引用类型,还可以使用C#的as操作符。下面对代码进行了修改,将操作符与Sting配合使用(因为Int32是一个值类型):
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 non-nullable value type. Consider using 'default(T)' instead
//无法将null转换为类型参数“T",因为它可能是不为null的值类型。请考虑改用default('T')
}
由于未对T进行约束,所以它可能是一个值类型,而将值类型的一个变量设为null是不可能的。如果T被约束成引用类型,将temp设为null就是合法的,代码能顺利编译并运行。
private static void SettingAGenericTypeVariableToDefaultValue<T>() { T temp = default(T); // OK }
以上代码中的default关键字告诉C#编译器和CLR的JIT编译器,如果T是一个引用类型,就将temp设为null;如果T是一个值类型,就将temp的所有位设为0。
3)、将一个泛型类型变量与null进行比较
无论泛型类型是否被约束,使用==或!=操作符将一个泛型类型变量与null进行比较都是合法的。
private static void ComparingAGenericTypeVariableWithNull<T>(T obj) { if (obj == null) { /* Never executes for a value type */ } //对于值类型来说,永远不会执行到判断里 }
注:如果T被约束成为一个struct,C#编译器会报错。值类型的变量不能与null进行比较,因为结果始终是相同的。
4)、两个泛型变量相互比较
如果泛型类型参数不是一个引用类型,对同一个泛型类型的两个变量进行比较是非法的。
private static void ComparingTwoGenericTypeVariables<T>(T o1, T o2) { if (o1 == o2) { } // Error }
在这个例子中,T为进行约束。虽然两个引用类型的变量相互比较是合法的,单两个值类型的变量相互比较是非法的,除非值类型重载了 ”==“ 操作符。
不允许将类型参数约束成一个具体的值类型,因为值类型隐式密封,不可能存在从值类型派生的类型。如果允许将类型参数约束成一个具体的值类型,那么泛型方法会被约束为只支持该具体类型,这还不如写一个非泛型的方法呢!
5)、泛型类型变量作为操作数使用
最后要注意的是,将操作符应用于泛型类型的操作数,会出现大量的问题。不能将这些操作符(+,-,*,/)应用于泛型类型的变量,因为编译器在编译时无法确定类型。
private static T Sum<T>(T num) where T : struct { T sum = default(T) ; for (T n = default(T) ; n < num ; n++) sum += n; return sum; }
将T约束成为一个struct,而且使用default(T)将sum 和n初始化为0.编译仍会报错。运算符”<","++","+="无法应用于“T"(和)”T"类型的操作数