C# 2.0 提出的泛型特性使类型可以被参数化,从而不必再为不同的而类型提供特殊版本的方法实现。泛型提供了代码重用的另一种机制,它不同于面向对象中通过继承方式实现代码重用,更准确地说,泛型锁提供的代码重用是算法的重用,即某个方法实现不需要考虑所操作数据的类型
泛型是什么
泛型英文是 ”generic“,这个单词意味 ”通用的“。字面意思上,泛型代表的就是 “通用类型”,它可以代替任意的数据类型,使类型参数化,从而达到只实现一个方法就可以操作多种数据类型的目的。泛型将方法实现行为与方法操作的数据类型分离,实现了代码重用。我们看下面代码示例
代码语言:javascript复制class Program
{
static void Main(string[] args)
{
List<int> intList = new List<int>();
List<string> stringList = new List<string>();
}
}
从以上代码中,List 是 .Net 类库中实现的泛型类型,T 是泛型参数(形参)如果想实例化一个泛型类型,就必须传入实际的类型参数,如代码中的 int 和 string,就是实际的类型参数。同时你也可以自己实现泛型类型
为什么要引入泛型
如果不引入泛型,会带来怎样的不便?
我写了两个比较大小的函数,如下所示,前者是针对整型,后者是针对字符串的。如果我想比较浮点数大小,那么又要再新增一个函数
代码语言:javascript复制public class Compare
{
public static int CompareInt(int int1, int int2)
{
if (int1.CompareTo(int2) > 0)
{
return int1;
}
else
{
return int2;
}
}
public static string CompareString(string s1, string s2)
{
if (s1.CompareTo(s2) > 0)
{
return s1;
}
else
{
return s2;
}
}
}
如果用泛型呢?
代码语言:javascript复制public class Compare<T> where T : Icomparable
{
public static T CompareGeneric(T t1, T t2)
{
if (t1.CompareTo(t2) > 0)
{
return t1;
}
else
{
return t2;
}
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine(Compare<int>.CompareGeneric(3, 4));
Console.WriteLine(Compare<string>.CompareGeneric("aa", "aaa"));
}
}
有了泛型,就不需要再针对每种数据类型重复实现相似的比较方法了
泛型除了可以实现代码的重用,还提供了更好的性能和类型安全特性。我们知道引用类型和值类型间存在着相互转换,转换的过程称为装箱和拆卸,这对过程会引起一定的性能损失,而泛型是避免性能损失的有效方法
全面解析泛型
类型参数
在前面的泛型代码中,T 就是类型参数。无论调用类型方法还是初始化泛型实例,都需要用真实类型来代替 T。你可以把 T 理解为类型的一个占位符,即告诉编译器,在调用泛型时必须为其指定一个实际类型。
根据泛型类型参数是否提供实际类型,又可把泛型分为两类:
- 未绑定的泛型:没有为类型参数提供实际类型
- 已构造的泛型:已指定了实际类型作为参数
已构造的泛型又可分为:
- 开放类型:包含类型参数的泛型
- 密封类型:已经为每一个类型参数都传递了实际数据类型的泛型
下面代码演示了判断泛型类型是开放还是封闭的方法
代码语言:javascript复制public class DictionaryStringKey<T> : Dictionary<string, T> {}
class Program
{
static void Main(string[] args)
{
// Dictionary<,> 是一个开放类型,它有两个类型参数
Type t = typeof(Dictionary<,>);
// DictionaryStringKey<> 也是一个开放类型,但它只有一个类型参数
t = typoef(DictionaryStringKey<>);
// DictionaryStringKey<int> 是一个封闭类型
t = typeof(DictionaryStringKey<int>);
// Dictionary<int, int> 是一个封闭类型
t = typeof(Dictionary<int, int>);
}
}
泛型中的静态字段和静态函数
静态数据类型是属于类型的。对于静态字段来说,如果在某个MyClass
类中定义了一个静态字段 x,则不管之后创建了多少个该类的实例,也不管从该类派生出多少个实例,都只存在一个MyClass.x
字段。但泛型类型并非如此。每个封闭的泛型类型中都有仅属于它自己的静态数据,如下所示
public static class TypeWithStaticField<T>
{
public static string field;
public static void OutField()
{
Console.WriteLine(field ":" typeof(T).Name);
}
}
class Program
{
static void Main(string[] args)
{
TypeWithStaticField<string>.OutField(); // :String
TypeWithStaticField<int>.OutField(); // :Int32
Console.ReadKey();
}
}
对于编译器来说,每个封闭泛型类型都是一个不一样的类型,所以它们都有属于它自己的静态字段
代码语言:javascript复制public static class GenericClass<T>
{
static GenericClass()
{
Console.WriteLine("泛型静态类型构造函数调用了");
}
public static void Print()
{
}
}
class Program
{
static void Main(string[] args)
{
GenericClass<string>.Print(); // 泛型静态类型构造函数调用了
GenericClass<string>.Print();
GenericClass<int>.Print(); // 泛型静态类型构造函数调用了
Console.ReadKey();
}
}
类型参数的推断
由于使用泛型时都需要写“<”和“>”等符号,在阅读代码时,一旦代码变多,难免另开发人员感觉头晕。通过使用编译器的类型推断,你可以在写泛型代码时省略掉这些符号,具体的实际类型则由编译器自选推断
代码语言:javascript复制public static class GenericClass
{
public static void Print<T>(T t)
{
Console.WriteLine(t.ToString());
}
}
class Program
{
static void Main(string[] args)
{
GenericClass.Print("123"); // 不需要再写 <> 了
GenericClass.Print(123);
Console.ReadKey();
}
}
在以上代码中,编译器会根据传递的方法实参来判断传入的实际类型参数。如果编译器根据传入的参数不能推断出实际参数类型,就会出现编译错误
类型参数的约束
先看下面这段代码
代码语言:javascript复制private static T Max<T>(T obj1, T obj2)
{
if (obj1.CompareTo(obj2) > 0) // 编译器报错:T 不包含 CompareTo 的定义
{
return obj1;
}
return obj2;
}
T 表示任意类型,可是很多类型没有实现CompareTo
方法。如此,你自然相对类型参数做出一定的约束,限制类型参数只能代表某些符合要求类型,这就是我们使用类型约束的目的,也促使了“类型参数约束”的诞生
private static T Max<T>(T obj1, T obj2) where T : IComparable<T>
{
if (obj1.CompareTo(obj2) > 0)
{
return obj1;
}
return obj2;
}
类型约束就是用 where 关键字来限制某个类型实参的类型
C# 中有四种约束可以使用,它们的语法类似:约束要放在泛型方法或类型声明的末尾,并且要使用 where 关键字
1、引用类型约束
引用类型约束的表示形式为T:class
,它确保传递的类型实参必须是引用类型
注意,约束的类型参数和类型本身没有关系,即在定义一个泛型结构体时,泛型类型一样可以被约束为引用类型。但是注意,你不能制定以下这些特殊的引用类型:System.Object
、System.Array
、System.Delegate
、System.MulticastDelegate
、System.ValueType
、System.Enum
和System.Void
2、值类型约束
值类型约束的表示形式为T:struct
,它确保传递的类型实参时值类型(包括枚举),但这里的值类型不包括可控类型
public class SampleValueType<T> where T : struct
{
public static T Test()
{
return new T();
}
}
static void Main(string[] args)
{
SampleValueType<string>.Test(); // 类型 string 必须是不可为 null 值的类型
SampleValueType<float>.Test();
Console.ReadKey();
}
在以上代码中,new T() 是可以编译通过的,因为 T 是一个值类型,所有值类型都有一个公共的无参构造函数,但如果不对 T 进行约束,或约束为引用类型,则上面的代码就会报错,因为有的引用类型是没有无参构造函数的
3、构造函数类型约束
构造函数类型约束的表示形式为 T : new(),如果类型参数有多个约束,则此约束必须最后指定。构造函数类型约束确保指定的类型实参有一个公共午餐构造函数的非抽象类型。这适用所有值类型,所有非静态、非抽象、没有显示声明构造函数的类,以及显示声明了一个公共无参的构造函数的所有非抽象类
代码语言:javascript复制public class SampleValueType<T> where T : class, new()
{
public static T Test()
{
return new T();
}
}
4、转换类型约束
转换类型约束的表示形式为:
- T : 基类名(确保指定的类型实参必须是基类或派生自基类的子类)
- T : 接口名(确保指定的类型实参必须是接口或实现了该接口的类)
- T : U(确保提供的类型实参必须是 U 提供的类型实参或者是派生于 U 提供的类型实参)
5、组合约束
组合约束是将多个不同种类的约束合并到一起的情况
代码语言:javascript复制public class SampleValueType<T> where T : class, IDisposable, new()
{
}
public class SampleValueType<T, U> where T : class, IDisposable, new() where U: struct
{
}