在2005年底微软公司正式发布了C# 2.0,与C# 1.x相比,新版本增加了很多新特性,其中最重要的是对泛型的支持。通过泛型,我们可以定义类型安全的数据结构,而无需使用实际的数据类型。这能显著提高 性能并得到更高质量的代码。泛型并不是什么新鲜的东西,他在功能上类似于C++的模板,模板多年前就已存在C++上了,并且在C++上有大量成熟应用。
本文讨论泛型使用的一般问题,比如为什么要使用泛型、泛型的编写方法、泛型中数据类型的约束、泛型中静态成员使用要注意的问题、泛型中方法重载的问、泛型方法等,通过这些使我们可以大致了解泛型并掌握泛型的一般应用,编写出更简单、通用、高效的应用系统。
什么是泛型
为什么要使用泛型 public class Stack { private int[] m_item; public int Pop(){...} public void Push(int item){...} public Stack(int i) { this.m_item = new int[i]; } } 上 面代码运行的很好,但是,当我们需要一个栈来保存string类型时,该怎么办呢?很多人都会想到把上面的代码复制一份,把int改成string不就行 了。当然,这样做本身是没有任何问题的,但一个优秀的程序是不会这样做的,因为他想到若以后再需要long、Node类型的栈该怎样做呢?还要再复制吗? 优秀的程序员会想到用一个通用的数据类型object来实现这个栈: public class Stack { private object[] m_item; public object Pop(){...} public void Push(object item){...} public Stack(int i) { this.m_item = new[i]; }
} 这个栈写的不错,他非常灵活,可以接收任何数据类型,可以说是一劳永逸。但全面地讲,也不是没有缺陷的,主要表现在: 当Stack处理值类型时,会出现装箱、折箱操作,这将在托管堆上分配和回收大量的变量,若数据量大,则性能损失非常严重。 stack.Push(x); Node2 y = (Node2)stack.Pop(); 上面的代码在编译时是完全没问题的,但由于Push了一个Node1类型的数据,但在Pop时却要求转换为Node2类型,这将出现程序运行时的类型转换异常,但却逃离了编译器的检查。
针对object类型栈的问题,我们引入泛型,他可以优雅地解决这些问题。泛型用用一个通过的数据类型T来代替object,在类实例化时指定T的类型,运行时(Runtime)自动编译为本地代码,运行效率和代码质量都有很大提高,并且保证数据类型安全。
使用泛型 public class Stack<T> { private T[] m_item; public T Pop(){...} public void Push(T item){...} public Stack(int i) { this.m_item = new T[i]; } } 类的写法不变,只是引入了通用数据类型T就可以适用于任何数据类型,并且类型安全的。这个类的调用方法: //实例化只能保存int类型的类 Stack<int> a = new Stack<int>(100); a.Push(10); a.Push("8888"); //这一行编译不通过,因为类a只接收int类型的数据 int x = a.Pop();
//实例化只能保存string类型的类 Stack<string> b = new Stack<string>(100); b.Push(10); //这一行编译不通过,因为类b只接收string类型的数据 b.Push("8888"); string y = b.Pop();
这个类和object实现的类有截然不同的区别: 1. 他是类型安全的。实例化了int类型的栈,就不能处理string类型的数据,其他数据类型也一样。 2. 无需装箱和折箱。这个类在实例化时,按照所传入的数据类型生成本地代码,本地代码数据类型已确定,所以无需装箱和折箱。 3. 无需类型转换。
泛型类实例化的理论 泛型类的不同的封闭类是分别不同的数据类型。 例:Stack<int>和Stack<string>是两个完全没有任何关系的类,你可以把他看成类A和类B,这个解释对泛型类的静态成员的理解有很大帮助。
泛型类中数据类型的约束 public class Node<T, V> where T : Stack, IComparable where V: Stack {...} 以上的泛型类的约束表明,T必须是从Stack和IComparable继承,V必须是Stack或从Stack继承,否则将无法通过编译器的类型检查,编译失败。 通 用类型T没有特指,但因为C#中所有的类都是从object继承来,所以他在类Node的编写中只能调用object类的方法,这给程序的编写造成了困 难。比如你的类设计只需要支持两种数据类型int和string,并且在类中需要对T类型的变量比较大小,但这些却无法实现,因为object是没有比较 大小的方法的。 了解决这个问题,只需对T进行IComparable约束,这时在类Node里就可以对T的实例执行CompareTo方法了。这个问题可以扩展到其他用 户自定义的数据类型。 如果在类Node里需要对T重新进行实例化该怎么办呢?因为类Node中不知道类T到底有哪些构造函数。为了解决这个问题,需要用到new约束: public class Node<T, V> where T : Stack, new() where V: IComparable 需要注意的是,new约束只能是无参数的,所以也要求相应的类Stack必须有一个无参构造函数,否则编译失败。 C#中数据类型有两大类:引用类型和值类型。引用类型如所有的类,值类型一般是语言的最基本类型,如int, long, struct等,在泛型的约束中,我们也可以大范围地限制类型T必须是引用类型或必须是值类型,分别对应的关键字是class和struct: public class Node<T, V> where T : class where V: struct
泛型方法 public class Stack2 { public void Push<T>(Stack<T> s, params T[] p) { foreach (T t in p) { s.Push(t); } } } 原来的类Stack一次只能Push一个数据,这个类Stack2扩展了Stack的功能(当然也可以直接写在Stack中),他可以一次把多个数据压入Stack中。其中Push是一个泛型方法,这个方法的调用示例如下: Stack<int> x = new Stack<int>(100); Stack2 x2 = new Stack2(); x2.Push(x, 1, 2, 3, 4, 6); string s = ""; for (int i = 0; i < 5; i++) { s += x.Pop().ToString(); } //至此,s的值为64321
泛型中的静态成员变量 这也非常容易理解,因为不同的封闭类虽然有相同的类名称,但由于分别传入了不同的数据类型,他们是完全不同的类,比如: Stack<int> a = new Stack<int>(); Stack<int> b = new Stack<int>(); Stack<long> c = new Stack<long>(); 类实例a和b是同一类型,他们之间共享静态成员变量,但类实例c却是和a、b完全不同的类型,所以不能和a、b共享静态成员变量。 泛型中的静态构造函数 泛型中的静态构造函数的原理和非泛型类是一样的,只需把泛型中的不同的封闭类理解为不同的类即可。以下两种情况可激发静态的构造函数: 1. 特定的封闭类第一次被实例化。 2. 特定封闭类中任一静态成员变量被调用。
泛型类中的方法重载 public class Node<T, V> { public T add(T a, V b) //第一个add { return a; } public T add(V a, T b) //第二个add { return b; } public int add(int a, int b) //第三个add { return a + b; } } 上面的类很明显,如果T和V都传入int的话,三个add方法将具有同样的签名,但这个类仍然能通过编译,是否会引起调用混淆将在这个类实例化和调用add方法时判断。请看下面调用代码: Node<int, int> node = new Node<int, int>(); object x = node.add(2, 11); 这个Node的实例化引起了三个add具有同样的签名,但却能调用成功,因为他优先匹配了第三个add。但如果删除了第三个add,上面的调用代码则无法编译通过,提示方法产生的混淆,因为运行时无法在第一个add和第二个add之间选择。 Node<string, int> node = new Node<string, int>(); object x = node.add(2, "11"); 这两行调用代码可正确编译,因为传入的string和int,使三个add具有不同的签名,当然能找到唯一匹配的add方法。 由以上示例可知,C#的泛型是在实例的方法被调用时检查重载是否产生混淆,而不是在泛型类本身编译时检查。同时还得出一个重要原则: 当一般方法与泛型方法具有相同的签名时,会覆盖泛型方法。
泛型类的方法重写
泛型的使用范围 小结 |