使用泛型的主要目的有三个:(1)实现代码复用;(2)避免使用Object类,在实例化一个泛型类时,我们需要指定T的实际类型(类型实参),这样保证了类型安全;(3)减少了 Object 造成的装箱拆箱,提高性能(原理见下文)。
对于编译器而言,泛型 T 本质上就是一个 类型参数(Type parameter),所谓参数其实就是一个特殊的占位符。泛型被定义在程序集中,因此当代码被编译成 IL 放到程序集中时,T 仍然存在,例如以下案例:
Console.WriteLine(typeof(List<>));
Console.WriteLine(typeof(Dictionary<,>));
//以下是输出
System.Collections.Generic.List`1[T]
System.Collections.Generic.Dictionary`2[TKey,TValue]
其中,后单引号(`)后面的数字代表类型参数的个数。
在代码运行时,即在 JIT 阶段,IL 被翻译成 本机语言时,泛型类型参数 T 会被替换成具体的类型。例如,如果你使用了 List
类,那么代码中的 T 是在 JIT 翻译之后才变成了 int 类型。因此,不存在类型转换、装箱拆箱,这就是性能更好的原因。
需要注意,未指定类型实参的泛型类型是不能实例化的,因为它是一种 开放类型,这与你不能实例化一个接口一样。传递一个 类型实参 (Type argument)后,变成 封闭类型,才可以实例化。案例:
Object o;
o = Activator.CreateInstance(typeof(List<int>));//OK
o = Activator.CreateInstance(typeof(List<>));//抛出ArgumentException,因为含有泛型参数
泛型最常用的地方是集合类。微软建议使用泛型集合,不建议使用非泛型集合,除了上文中提到的类型安全、性能高之外,泛型集合类中的虚方法更少,从而进一步提高执行性能;另外,泛型集合一般拥有更多的扩展方法,使用更方便。
泛型类可以派生自一个泛型基类,但是,泛型子类必须重复泛型基类的泛型类型,或者必须指定基类的泛型类型。
例如,考察以下代码:
public class ChildGeneric : Generic<T>{} //编译失败
public class ChildGeneric<T> : Generic<T>{} //编译通过
public class ChildGeneric : Generic<int>{} //编译通过
考察两个泛型类型 List
和 List
,其中 MyChildClass
派生自 MyBaseClass
,那么 List
和 List
之间有什么关系吗?答案是没有关系。
因为 类型参数 的继承关系 不改变 泛型类型的 继承关系,或者说,泛型类型 破坏了 泛型类型参数的继承关系。
更具体一点,指定类型实参并不影响层次结构。List
派生自 Object
,那么 List
和 List
都是从 Object
派生,二者是 “平辈” 的。指定类型实参只是在 JIT 时拿指定的类型替换 T,这两个 List 是两个不同的类。
由此引出另一个话题,即 逆变 和 协变,详见我的另一篇文章《C#与CLR学习笔记(6)—— 轻松理解协变与逆变》
由于泛型参数 T 在定义时并未指定,因此,为了安全性,编译器会在编译时进行分析,确保代码适用于未来可能指定的任何泛型类型实参。
例如,考察如下代码:
private static T Min<T>(T o1, T o2)
{
if (o1.CompareTo(o2) < 0)
return o1;
return o2;
}
上述代码编译失败,因为并非所有类型都实现了 IComparable
接口,因此 CompareTo()
方法有无法执行的风险。
所以从表面上看,使用泛型似乎做不了太多事情,只能使用 Object
中定义的方法。显然,实际情况并不是这样,因为有 泛型约束 机制,它使得泛型变得有用。
上面的案例改进一下,就可以顺利编译:
private static T Min<T>(T o1, T o2) where T : IComparable<T>
{
if (o1.CompareTo(o2) < 0)
return o1;
return o2;
}
约束 可应用于 泛型类型 或 泛型方法 。如果 基类 或者 被重写/实现的方法 拥有泛型约束,那么子类或其方法必须应用同样的泛型约束。
例如:
public class Generic<T> where T : struct
{}
public class ChildGeneric<T> : Generic<T> //编译失败
{}
public class ChildGeneric<T> : Generic<T> where T : struct //编译通过
{}
public interface IGeneric2
{
string GetTInfo<T>() where T : struct;
}
class ChildGeneric : IGeneric2
{
public string GetTInfo<T>() where T : class //编译失败
{
return "";
}
}
class ChildGeneric : IGeneric2
{
public string GetTInfo<T>() where T : struct//编译通过
{
return "";
}
}
分类 | 形式 | 含义 |
---|---|---|
主要约束 (最多指定一个) |
where T : struct |
T 必须是值类型 ( Nullable 除外,详见参考文献) |
where T : class |
T 必须是引用类型 | |
where T : Foo |
T 必须派生自 Foo 类 | |
次要约束 (可以指定多个) |
where T : IFoo |
T 必须实现 IFoo 接口 |
where T1 : T2 |
T1 派生自 泛型类型 T2 | |
构造器约束 | where T : new() |
T 是拥有公共无参构造函数的非抽象类 |
(1)类型转换问题
泛型参数进行类型转换,要保证符合约束。尽量使用 as
进行转换。
public void DoSomething<T>(T obj)
{
int x = (int)obj; //编译错误
int x = (int)(Object)obj; //编译通过,但运行时可能抛 InvalidCastException
stirng x = obj as string; //推荐
}
(2)将泛型类型转为默认值
推荐使用 default
关键字。
public void DoSomething<T>()
{
T temp = null; //编译错误,除非指定 T : class
T temp = default(T); //推荐
}
(3)两个泛型参数对比
需指定泛型约束。
public void DoSomething<T>(T o1, T o2)
{
if (o1 == o2){} //编译失败,因为若 T 是值类型,那么可能没有重载 == 运算符。
}
(4)不能将泛型约束为具体的值类型,一是因为值类型是隐式密封的,无法被继承;二是因为这种情况使用泛型就没有意义。
[1] 《CLR via C#》 第四版