首先陈述,这是一篇水文,因为文字内容比较干,主要是一些介绍和概念。但对泛型理解还是有一定的帮助。没有过多的代码解释,勿怪。
引入:.net2.0发布之后,C#编程语言开始支持泛型,用以增强它的特性,基于此,在基础类库中引入了一个以集合为中心的新命名空间:System.Collections.Generic命名空间。
第一个问题:为什么要引入泛型。其实也就是和数组之间的区别之处。
1、之前提到过C#的数组,数组这种数据结构可以提供一组固定上限的同一类型项。但有很多时候,我们却需要更灵活的数据结构,例如,可以增长和收缩的容器。以及容器可以保存符合某个条件的对象。
2、类型安全和性能优势。(约束)
既然如此,那么就先从非泛型集合开始看起,逐渐过渡。
一、非泛型集合
我们知道,我们经常用mscorlib.dll中的System.Collections命名空间中很多类来组织数据。例如,ArrayList、Hashtable、Queue、Stack、SortedList。
使用这些类将会导致许多问题。
1、低性能。特别在操作数据结构的时候(值类型)。当我们在经典的集合类中存储结构时,CLR必须执行大量的内存转换。
2、有些经典的集合类不是类型安全的,因为他们是为了操作System.Object类而开发的。因此,可以包含任何类型。
强调:任何使用.net2.0或者更高版本创建的项目都应该放弃使用System.Collections中的类,而使用System.Collections.Generic中的类。
第一个问题,性能
之前提到,.net支持两大数据类型:值类型和引用类型。有时候我们需要使用一个类别的变量表示另一个类别的变量,于是C#提供了成为装箱的简单机制。这允许我们将值类型保存在引用类型变量中。
装箱:显示的将值类型分配给System.Object变量的过程。这个过程,CLR会在堆上分配新的对象并将值类型的值复制到那个实例上,返回给我们的是新分配在堆上对象的引用。
拆箱:与装箱相反,把保存在对象引用中的值转换回栈上的相应值类型。(更像是类型转换操作,但语义上却迥然不同,因为拆箱CLR会验收值类型是不是等价于装箱的类型,如果是则将值类型复制回本地栈变量上。)所以与转换不同,拆箱必须回到合适的数据类型。
注意:Console.WriteLine()方法操作的对象也是Object类型,故而当输出的时候,我们的值会被再次装箱。
装箱拆箱发生的步骤如下:
1.必须在托管堆上分配一个新对象。
2。基于栈数据的值必须被转移到新分配的内存位置。
3.在拆箱时,保存在堆上的对象中的值必须转移回栈。
4.堆上无用的对象(最后)会被回收。
由此可见,当我们操作大数据量的时候,建会产生大量的装箱拆箱操作,性能就是一个很大的问题了。理想情况下,我们应该可以在没有任何性能问题的容器中操作站数据,并且在获取数据的时候也不必使用try、catch作用域。(这正是泛型所实现的。)
第二个问题,类型安全
前面提到,在System.Collections命名空间下大多数类所操作的都是Object类型,因此他们可以容纳任何类型,这就导致了类型安全问题。
而我们大多数情况下,需要一个类型安全的容器来操作特定的数据类型。而为了做到这些,我们需要自定义类型集合。
然而,自定义集合,却并没有消除装箱拆箱的损失,例如不管用哪种类型来保存整数,我们都不可避免的使用非泛型容器带来的装箱问题。
这就需要使用泛型,泛型可以消除上述带来的损耗和安全问题。与非泛型相比,泛型的优势如下:
1、更好的性能,不会导致装箱拆箱损耗。
2、类型安全,他们只包含指定类型。
3、大幅度减少了构建自定义类型集合的需要,因为基础类库提供了几个预制的容器。
二、泛型类型参数的作用。
例如:List<T>
尖括号中的标记的正式名称为类型参数,通俗的可以将其称为占位符。<T>读作of T。
在创建泛型对象、实现泛型接口或调用泛型成员时,为泛型参数提供的值是由开发者决定的。
1、当我们为泛型类或结构指定特定的类型参数时候,例如Person类的泛型集合,List<Person>。当创建List<T>变量的时候,编译器并没有为List<T>创建全新的实现,而只处理你实际调用的泛型类型的成员。
2、除了可以给泛型类指定类型成员,还可以死给泛型成员指定类型成员(即方法和属性。)。例如List<int> intList;还有泛型接口,interface IComparable<T>
三、System.Collections.Generic命名空间
之前我们学习过对象初始化语法,现在我们看一下集合初始化语法:
这种语法特性,让你可以用与填充基础数组类似的语法,来填充ArrayList或List<Y>等容器。
说明,只能支持Add()方法的类使用集合初始化语法,这是ICollection<T>或ICollection接口决定的。
如建立一个point集合类,可以如下表示:
List<Point> mylit = new List<Point>
{
new point{x=1,y=1},
new point{x=2,y=2},
...
};
这种语法的好处是减少键盘输入,但是坏处则是影响可读性。
此命名空间下,包括几种泛型类。
List<T>,使用最广,用以动态调整的类型。
Stack<T>,栈,后进先出的数据集合,包含push和pop方法,分别比欧式进栈和出栈。
Queue<T>,队列,先进先出的数据集合,包括Dequeue和Enqueue方法,分别表示在移除开始处对象(并返回)和在队列末尾添加一个对象,除此之外,还有Peek方法,返回队列开始处对象,但并不移除。
SortedSet<T>,这个类中的项是自动排序的,在插入和移除之后,也能自动排序,因此该类十分有用。
四、创建自定义的泛型方法/泛型结构和类。
传统方法,交换两个整数:
Swap(ref int a, ref int b){ int temp; temp = a; a = b; b = temp;}
如果我们继续要交换两个Person对象,则我们必须编写一个swap的新版本:
Swap(ref Person a, ref Person b){ Person temp; temp = a; a = b; b = temp;}
如果需要交换其他的数据,则需要定义更多的方法,这就会给维护带来麻烦,如果想要有单一的方法,我们需要创建操作Object类型的方法,但这样会导致装箱、拆箱,类型安全,显示转换等问题。
如果你要创建的方法重载只是输入参数不同,可以使用泛型。方法如下:
Swap<T>(ref T a, ref T b){T temp; temp = a; a = b; b = temp;}
我们还可以创建泛型类或结构:public class Point<T>{}
其中,我们可以是一个双精度的point也可以是一个int的point。
1、default(T),和泛型一块儿使用的时候,它表示一个类型参数的默认值。这非常有用,因为一个泛型类型预先并不知道实际的真为辅,因此无法安全的假设默认值是什么。默认值如下:
数值的默认值为0;引用类型的默认值是null;一个结构的字段被设为0(值类型)或null(引用类型)。
2、泛型基类,泛型类可以作为其他类的基类,它可以定义许多虚方法和抽象方法。但是要做到这些,泛型类需要遵循一些守则:
首先,如果一个非泛型类型扩展了一个泛型类,派生类必须指定一个类型参数。public class B:A<string>{}.其中A类为我们自定义的泛型类。
其次,如果泛型基类定义了泛型虚方法或抽象方法,派生类型必须使用指定类型参数重写泛型方法。
最后,如果派生类型也是泛型,则它能够(可选的)重用类型占位符。不过要注意派生类必须遵照基类中的任何约束。
五,类型参数的约束
如本节描述,任何泛型都必须至少有一个类型参数,并在与泛型类型或参数交互时指定该类型参数,这可以使我们构建类型安全的代码。
.net平台使用where关键字可以得到更加具体的类型参数信息。下面几种约束表示如下:
where T:struct,该类型参数<T>必须在其继承链中包含System.ValueType值类型,即必须为结构。
where T : class,和上面相反,不能包含值类型,必须为引用类型。
where T : new(),该类型参数<T>必须包含一个默认的构造函数,因为无法预知子定义构造函数格式,所以如果泛型类型必须创建一个类型参数的实例,这将是非常有用的。注意,在有多个约束的时候,此约束必须列在末尾。
where T : NameOfBaseClass,该类型参数<T>必须派生于NameOfBaseClass指定的类。
where T : NameOfInterface,同上,必须派生于指定接口,多个接口必须用逗号隔开。
例如,如果要指定泛型智能操作结构,可以这样:
Swap<T>(ref T a, ref T b) where T : struct {....}。以这种方式约束了Swap方法就不能再对其他如string对象进行交换了,因为string是引用类型。
六,小结
本小节主要陈述了泛型对比之前的经典容器有的一些优势。类型安全、高效、减少代码量。除此之外,还知道了泛型约束的一些知识。