C# 泛型
一、泛型
假设我要写个公用的输出传入参数的方法(不用泛型),因为万物皆对象的理由,我先定义一个方法show(object obj),如下面所示:
public static void Show(object obj) { Console.WriteLine(obj.ToString()); }
执行这个方法
int i = 1; //装箱 Show(i);
如果传入的是值类型,值类型转换为引用类型,我们知道会发生装箱,这是对性能的损害,想想如果是个集合,就得多次执行装箱、拆箱操作。如ArrayList类,ArrayList储存对象,Add()方法定义为需要把一个对象作为参数,如果传入的值类型,就得装箱,在读取ArrayList中的值时,又得进行拆箱,如下面代码所示:
var list = new ArrayList(); list.Add(1); //装箱 foreach (int i in list) { Console.WriteLine(i); //拆箱 }
如果使用泛型,就不会出现这样的问题了,我们使用List<T>类来改造上面代码:
var list = new List<int>(); list.Add(1); foreach (int i in list) { Console.WriteLine(i); }
这里就不存在装箱和拆箱了,所以我们在使用集合的时候,尽量使用泛型集合,不要使用非泛型集合。
二、类型安全
在上面ArrayList类中,添加参数时,可以添加任何对象,比如上面的例子,如果在添加整数类型后再添加引用类型,这么做在编译时是没有任何问题,但是在foreach语句使用整数类型迭代的时候就会报错。
var list = new ArrayList(); list.Add(1); //装箱 list.Add("string"); foreach (int i in list) { Console.WriteLine(i); }
这时候就会报InvalidCastException的异常。
如果使用泛型集合List<T>的时候去重写上面的代码,在编译的时候就会报错。所以这个地方我们就能知道,泛型是在编译时就已经执行了,所以系统运行时我们时没有装箱拆箱的系统开销,而非泛型是在运行时执行的,所以可能导致异常发生;
三、创建泛型类和泛型方法
泛型方法,从我最先第一个例子Show(object) ,采用泛型来重写,定义为Show<T>(T);
public static void Show<T>(T obj) { Console.WriteLine(obj.ToString()); }
泛型类,如public class List<T>{}
3.1 命名约定
- 泛型类型的名称用字母T作为前缀。
- 如果没有特殊的要求,泛型类型运行用任意类替代,且只使有一个泛型类型,就可以用字符T作为泛型类型的名称。
- 如果泛型类型有特殊的要求(如它必须实现一个接口或派生自基类),或者使用了两个或以上的泛型类型,就应给泛型类型使用描述性的名称:
public delegate void EventHandler<TEventArgs>(object sender,TEventArgs e);
public delegate TOutput Convert<TInput,TOutput>(TInput input);
public class SortedList<TKey,TValue>{};
3.2 默认值
在泛型类和泛型方法中产生的一个问题是,在预先未知以下情况时,如何将默认值分配给参数化类型 T,给定参数化类型 T 的一个变量 t,只有当 T 为引用类型时,语句 t = null 才有效;只有当 T 为数值类型而不是结构时,语句 t = 0 才能正常使用。 解决方案是使用 default 关键字,此关键字对于引用类型会返回 null,对于数值类型会返回零。 对于结构,此关键字将返回初始化为零或 null 的每个结构成员。
使用方式如:T obj=default(T);
3.3 约束
在定义泛型类时,可以对客户端代码能够在实例化类时用于类型参数的类型种类施加限制。 如果客户端代码尝试使用某个约束所不允许的类型来实例化类,则会产生编译时错误。 这些限制称为约束。 约束是使用where上下文关键字指定的。 下表列出了六种类型的约束:
约束 | 说明 |
where T:struct | 对于结构的约束,类型T必须是值类型。 |
where T:class | 类的约束,类型T必须是应用类型。 |
where T:<接口名称> | 类型参数必须是指定的接口或实现指定的接口。 可以指定多个接口约束。 约束接口也可以是泛型的。 |
where T:<基类名> | 类型参数必须是指定的基类或派生自指定的基类。 |
where T:new() | 类型参数必须具有无参数的公共构造函数。 当与其他约束一起使用时,new() 约束必须最后指定。 |
where T1:T2 | 类型T1必须是类型T2或派生自泛型类型T2,该约束也称为裸型约束。 |
public class MyClass<T> where T : IComparer<T>, new() { }
上面代码,使用泛型类型添加了两个约束,声明指定类型T必须实现了IComparer接口,且必须有一个默认构造函数
public class MyClass<TOutput, TInput> where TOutput : IComparer<TOutput>, new() where TInput:class,TOutput { }
上面代码用了两个泛型类型,TOutput必须实现了IComparer接口,且必须有一个默认构造函数,TInput必须是引用类型,且类型必须是TOutput或派生自TOutput。
3.4 继承
泛型类型可以实现泛型接口,也可以派生自一个类。泛型类型可以派生自泛型基类,其要求必须重复接口的泛型类型,或者必须指定基类的类型。如下列所示:
public class BaseClass<T> { } ///必须重复接口\基类的泛型类型 public class MyClass<T> : BaseClass<T> { }
public class BaseClass<T> { } ///必须指定基类的类型 public class MyClass<T> : BaseClass<String> { }
派生类可以是泛型类或非泛型类,例如定义一个抽象的泛型基类,它在派生类中用一个具体的类型实现,如下列所示:
public abstract class Calcu<T> { public abstract T Add(T x, T y); public abstract T Sub(T x, T y); } /// <summary> /// 派生类中具体的类型实现 /// </summary> public class IntCalcu : Calcu<int> { public override int Add(int x, int y) { return x + y; } public override int Sub(int x, int y) { return x - y; } }
四、结语
这些泛型类和泛型方法将一个或多个类型的指定推迟到客户端代码声明并实例化该类或方法的时候。 例如,通过使用泛型类型参数 T,您可以编写其他客户端代码能够使用的单个类,而不致引入运行时强制转换或装箱操作的成本或风险。在架构中有句话是让一切能延迟的延迟。