在C#引入泛型之前,如果一个类或者一个方法想要支持多种类型,它就不得不把相应的参数或者返回值定义为object类型,这就意味着代码在执行的过程中需要很多的转型,并不是说你的代码一定不要用到转型,但是转型确实会带来很多潜在的问题,因为它将本该属于编译时的类型检查延迟到了运行时,而且也会带来一定的性能问题(装箱和拆箱)。
C#2引入了泛型,包括两种形式的泛型:泛型类型和泛型方法。下面说一下和泛型有关的一些特性:
一、泛型约束
C#泛型支持四种类型的约束,但是它们使用同样的语法:
引用类型约束
对于引用类型的约束,泛型对象之间可以通过==和!=进行比较,但是需要注意的是,除非指定了其它约束,否则只会比较引用本身(即使引用对象类型重载了比较方法),使用转换类型约束,重载的比较方法才会被编译器调用。
class RefSample<T> where T : class
值类型约束
对于值类型的约束,泛型对象之间不允许使用==和!=进行比较。
class ValSample<T> where T : struct
构造函数类型约束
构造函数类型约束类型要包含无参构造函数。这样就可以构造泛型对象实例。
public T CreateInstance<T>() where T : new()
{
return new T();
}
转型类型约束
转型类型约束允许你指明一种类型,使得参数类型必须能被隐式转换成指明的类型。
转换类型约束非常重要,因为你可以用泛型对象调用指明类型的方法。例如,对于T : IComparable<T>,这意味着你能直接比较两个T类型的对象。
class Sample<T> where T : Stream,
IEnumerable<string>,
IComparable<int>
当几种约束混合使用的时候,实际上还是有一些规则的,
1)没有一个类型可以既是引用类型又是值类型,所以同时做引用类型约束和值类型约束是不允许的。
2)每一个值类型都有一个无参构造函数,所以不能同时有值类型约束和构造函数类型约束。
3)对于多个转换类型约束,最多只能有一个类类型,而且类类型必须放在接口类型的前边。而且每一个类型转换约束只能出现一次。
4)不同的参数类型可以有不同的约束,它们通过各自的where来指定。
下面列出了一些合法与不合法的约束混合使用:
Valid
class Sample<T> where T : class, IDisposable, new()
class Sample<T> where T : struct, IDisposable
class Sample<T,U> where T : class where U : struct, T
class Sample<T,U> where T : Stream where U : IDisposable
Invalid
class Sample<T> where T : class, struct
class Sample<T> where T : Stream, class
class Sample<T> where T : new(), Stream
class Sample<T> where T : IDisposable, Stream
class Sample<T> where T : XmlReader, IComparable, IComparable
class Sample<T,U> where T : struct where U : class, T
class Sample<T,U> where T : Stream, U : IDisposable
二、类型推断
C#编译器允许在调用泛型方法的时候不用显式指明参数类型,而是通过方法中传递的参数(注意不是返回值类型)进行类型推断,注意的是这只针对泛型方法有用(对泛型类型无效),例如:
static List<T> MakeList<T>(T first, T second)
...
List<string> list = MakeList<string>("Line 1", "Line 2");
对于这个泛型方法,编译器可以根据参数的类型推断出T,因此允许用户无需显式指明类型:
List<string> list = MakeList("Line 1", "Line 2");
三、默认值表达式
很多时候我们需要在泛型中使用默认值,C#使用default(T) 表达式返回当前泛型类型的默认值。
四、泛型中的比较
如果参数类型没有被约束,你只能使用==/!=运算符将值和null进行比较,不能用这两个操作符比较两个T类型的值。
如果参数类型被约束为值类型,那么==/!=根本就不能被应用。
如果参数类型被约束为引用类型,那么==/!=只会比较两者的引用。
如果参数类型被约束为为继承自重载了==/!=的类或接口,那么将使用重载的==/!=进行比较
例如:
static bool AreReferencesEqual<T>(T first, T second) where T : class
{
return first == second;
}
string str = "World";
string str1 = "Hello" + str;
string str2 = "Hello" + str;
Console.WriteLine(str1 == str2);
Console.WriteLine(AreReferencesEqual(str1, str2));
Output:
True
False
五、泛型中的静态字段与静态构造函数
对于泛型类型中的静态字段,它的每一个实例化类型都会拥有一份独立的静态字段。
class TypeWithField<T>
{
public static string field;
public static void PrintField()
{
Console.WriteLine(field + ": " + typeof(T).Name);
}
}
...
TypeWithField<int>.field = "First";
TypeWithField<string>.field = "Second";
TypeWithField<DateTime>.field = "Third";
TypeWithField<int>.PrintField();
TypeWithField<string>.PrintField();
TypeWithField<DateTime>.PrintField();
Output:
First: Int32
Second: String
Third: DateTime
所以一个通用的规则是:“one static field per closed type.”。这对静态构造函数同样适用,
六、JIT编译器怎样处理泛型
对于泛型类型,JIT会为每一个closed type创建它的本地代码,每一个closed type拥有自己的静态成员,而对于泛型方法或者泛型类型中的方法,JIT会对每一个值类型closed type创建不同的native code,但是对于所有引用类型closed type它们会共享同样的native code,其原因在于所有的引用都用同样大小的,所以其对象寻址方式是相同的。