一、泛型的定义及作用
泛型(generic)是C# 2.0推出的新语法,它是专门为处理多段代码在不同的数据类型上执行相同的指令的情况而设计的。比如说编程时,碰到功能非常相似的模块,只是它们所处理的数据类型不同,然而我们却需要写不同的方法来实现它,很明显,这加大了我们的工作量,也很乏味。有没有什么办法能够解决这个问题呢?它就是泛型了,可以让多个类型共享一组代码。通过压栈例子可以更清楚的了解泛型
class IntStack {int[] arr; public void push(int x) { ...}; //将int类型的值压栈 } class FloStack {float[] arr; public void push(float x) { ...};//将float类型的值压栈 }
这两个类功能一样,只是操作的数据类型不同,并且如果需要新类型(double、string)等时,又需要进行重复的操作,下面介绍怎么通过泛型解决这个问题
二、泛型的使用
class MyStack <T> { T[] arr; public void push(T x) { ...}; }
创建泛型类时,先在类名后面添加<T>,并将类型占位符T替代int或float。 由尖括号和T构成的字符串表明T是类型的占位符(不一定是字母T,也可以是其他标识符)。
泛型类型不是类型,而是类型的模板,就好比类型不是对象而是对象的模板一样。
C#提供了五种泛型:类、结构、接口、委托和方法。前面四个是类型,而方法是成员。还是很迷吧,接下来对五种泛型分别进行讲解。
1、泛型类
由于泛型类不是实际的类,而是类的模板,所以我们必须先从它们构造实际的类类型,然后创建这个构造后的类类型的实例。
从泛型类型创建实例的过程是:a:声明泛型类型 b:通过提供真实类型创建构造类型 c: 从构造类型创建实例 下面的例子有助于理解
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace generic { class Test<T1, T2>//构造泛型类,T1,T2为类型参数 { public T1 var1; public T2 var2; public void print() { Console.WriteLine("var1:{0} var2:{1}", var1 , var2); } } class Program { static void Main(string[] args) { //两种实例化的方式,效果相同 Test<int,string> first = new Test<int, string>();//int和string为类型实参,分别对应T1,T2 var second = new Test<string, int>(); first.var1 = 123; //first中,var1只能为int类型,当然也可以通过强制类型转换成其他的类型了 first.var2 = "Good Luck!"; //只能为string类型,同上 first.print(); second.var1 = "hello world"; second.var2 = 345; second.print(); Console.ReadKey(); } } }
同一个泛型可以构建出很多不同的类型,互不干扰,每一个都有独立的类类型,就好像有独立的非泛型类声明一样
2、泛型结构
struct MyStruct<T> { public T val; }
和泛型类类似,不过多叙述
3、泛型委托
泛型委托和非泛型委托非常相似,不过类型参数决定了它能接受什么方法
delegate S MyDel <T, S>(T value); //S为委托返回类型,T为委托参数类型
4、泛型接口
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace generic { interface Test<T> { void Print(T value); } /* * 泛型的接口实现必须唯一,必须保证类型参数组合不会再类型中产生两个重复的接口 * class Simple<S> : Test<int>, Test<S> 是错误的,因为S有可能是int类型 * 你也可以再非泛型类型中实现泛型接口 * class Simple : Test<int>, Test<string> */ class Simple<S> : Test<S> { public void Print(S value) { Console.WriteLine("value is {0}",value); } } class Program { static void Main(string[] args) { var IntSimp = new Simple<int>(); var StrSimp = new Simple<string>(); IntSimp.Print(123); StrSimp.Print("hello world"); Console.ReadKey(); } } }
注:泛型接口的名字不会和非泛型冲突,我们可以在前面的代码中声明一个Test的非泛型接口,程序能正常执行
5、泛型方法
泛型方法有两个参数列表,封闭在圆括号内的方法参数列表和封闭在尖括号内的类型参数列表,如下所示
public void Print<S,T> (S val1, T val2) { ... }
三、类型参数的约束(constraint)
顾名思义,即对类型参数进行约束,让编译器知道参数可以接受哪些类型,只有符合约束的类型才能替代给定的类型参数,来产生构造类型
对此,可以使用where子句,其语法为: where TypeParam : constraint, constraint ...
一个where对应一个类型参数,如果类型参数有多个约束,则用逗号分隔
class Test<T,S> where T : IComparable where S : new()
{...}
//不理解没关系,下面会讲 //注意两个where之间没有任何符号分隔,可以以任何次序列出
对于约束,有五种约束类型
where子句的约束必须有特定的顺序
a: 最多只能有一个主约束,如果有则必须放在第一位,主约束可以为结构、类或基类名
b: 可以有任意多的接口名约束,其类型为接口名称
c: 如果存在构造函数约束,则必须放在最后面,构造函数约束即为 new()
四、协变和逆变
“协变”是指能够使用与原始指定的派生类型相比,派生程度更大的类型。
“逆变”则是指能够使用派生程度更小的类型。
可以显式使用out关键字指定类型参数的协变,用in关键字指定类型参数的逆变
interface IItf<out T>{...} delegate void Test<in T>(T a);
显式变化使用in和out关键字只适合于委托和接口,不适用于类、结构和方法