第十章 集合与泛型
.NET 平台最基础的容器是 System.Array类型。但是许多时候需要更灵活的数据结构,为了理解构建灵活和安全类型的容器,本章首先介绍了System.Collections命名空间。
然而,在 .NET2.0 发布之后,C# 编程语言就增强了支持CTS的新特性泛型。大多数泛型都在System.Collections.Generic命名空间中。泛型容器和它的非泛型版本相比有很多优势。最后,研究了如何构建自己的泛型成员、类、结构和接口。
10.1 System.Collections的类型
之前用到最多的容器就是System.Array了,但是有很多限制,最大问题就是不能动态调整大小,因此就需要更灵活的容器,这就需要用System.Collections空间中的类型来实现。
首先,在这个命名空间中,有许多的接口,如Icomparer、IEnumerable、IEnumerator等,实现对容器的各种操作的定义(添加、删除、遍历等),然而,前面一章说过了,接口本身不是很有用,而是在各种类型中实现之后,方可显示他们的作用,而 System.Collections 中的类就实现了这些接口。实际上,这个空间中的各种类就是一个容器,用于承载其他类型,容器本身的意义不大,而主要的意义是对其中的内容进行操作(删除、插入、排序等)。
经常用到的类包括:
ArrayList:动态大小的数组。
可以随心所欲的调整内容的大小,利用AddRange批量填充数组,利用Insert插入到指定位置,ToaArray方法可以返回一个Object 类型的数组。
Hashtable:哈希表,是数值键标识的对象集合。
Queue:先进先出队列。
具有 Dequeue、Enqueue和Peek主要方法,完成出队、进队和查看队头元素。
Stack:后进先出队列(栈)。
具有Pop、Push、Peek 主要方法,完成出栈、进栈和查看栈顶元素。
SortedList:和字典相似,但是可以通过顺序位置进行访问。
由于这些都是一些应用类,因此没必要详细介绍,多看看 MSDN 就熟悉了,只要知道用途,在使用过程中自然都理解了。
另外,还有一些不常用的类,比如 bitarray 等,还有一些抽象基类,以供构造强类型的容器。
此外,System.Collections下还存在一个Specialized 命名空间,它定义了一组更特定的类型。不再赘述, MSDN 即可。
实际上,System.Collections和System.Collections.Specialized目前已经算是遗留类型了,这些类型不应该用于在.NET2.0 或更高版本中开发的新项目中。原因不是这些类型很危险,而是性能问题而缺少类型安全。新项目应该忽略这两个命名空间中的类型,而是用System.Collections.Generic 命名空间中相关类型。
10.2 遗留类型的问题
这里不直接进入泛型概念,而是先说说遗留类型存在的问题。
(1) 装箱、拆箱的问题
装箱就是通过把变量保存在System.object中(一定是保存在System.object才是装箱哦,非System.object 不是装箱),将值类型显式转换为相应的引用类型(显然引用类型之间的转换不属于此类问题)。如果装箱一个值,则 CLR 会分配新对象到堆上并且将值类型的值复制到那个实例中,因此,返回给我们得就是新分配对象的引用。
如:
short s=25;
object objshort=s;//object objshort=(object)s;也是可以的
疑问:系统是在哪里记住一个装箱操作的原来类型的?
疑问:装箱后,原来的栈中的数据怎么处理呢?立刻释放还是?
相反的操作叫拆箱。拆箱就是把保存在对象引用中的值类型转换回栈上的相应值类型。注意,拆箱首先会验证收到的值类型是不是等价于装箱的类型,如果是就将值复制回本地栈变量上。
short anothershrot=(short)objshort;
显然,拆箱必须是强制的,手动的。
int anotherint=(int)objshort;
注:这句也是强制转换,但是若由于转换类型不兼容,因此运行时是要抛出异常的。其实装箱和拆箱很有用,这使得我们把所有东西都当成 object 类型,而 CLR 回负责内存相关额细节。
例如对于 ArryaList 类,其中有一些方法:
1 |
public virtual int add( object value) |
5 |
public virtual object this ( int index) |
那么使用时:
1 |
ArrayList mylist= new ArrayList(); |
4 |
mylist.Add( new datatable()); |
疑问:拆箱后,将值复制回原来的变量位置?还是重新分配栈空间?
拆箱和装箱造成的主要问题在于:
1 )必须在托管堆上分配一个新的对象;
2 )基于栈数据的值必须被转移到那个内存位置;
3 )在拆箱时,保存在基于堆的对象中的值必须转移回栈;
4 )堆上无用的对象最后会被回收。
这就是性能方面的问题。另外,装箱和拆箱在类型安全方面,也有缺失。要使用语法进行拆箱,需要使用强制转换运算符,但是转换是成功还是失败要到运行时才能知道,如果尝试转换为错误的数据类型,就会抛出异常。这时,就需要将一组值类型保存在不需要装箱的容器中,这就是泛型可以解决的问题之一。
(2) 类型安全和强类型集合问题
在某些情况下,我们需要非常灵活的容器来保存所有东西。但是大多数时候希望是类型安全的容器,只可以操作某个类型的数据点。在引入.NET2.0泛型之前,程序员通过手动构建强类型集合来实现类型安全。
比如自定义了一个类person,要构建人员的一个容器(集合),可以在另外一个类,personcollection类中定义ArrayList成员(用于保存 person 类型,但是这个ArrayList是可以保存任意类型的),并且配置所有成员操作强类型的person对象而不是object类型。这样就不用担心类型安全了,因为C#编译器会检查任何尝试插入不兼容类型的请求(而不是在运行时)。虽然自定义的集合可以确保类型安全,但是必须每一个希望包含的类型创建一个自定义集合。但这不仅重复劳动,而且难以维护。泛型集合就允许我们推迟到创建时才制定包含的类型。
另外,尽管 .NET 类库中到处有强类型集合,但是这些自定义容器并没有解决装箱问题。即使只处理某种数据类型,还是要分配某个类型的对象来保存数据等。这时还是需要泛型类型,总结起来,泛型的优势:
1 )泛型提供更好的性能,因为他们不会导致装箱或拆箱损失;
2 )泛型更类型安全,因为他们只包含我们指定的类型;
3 )泛型大幅减少了构建自定义集合类型的需要,因为基类库提供了几个预置的容器。
10.3 System.Collections.Generic
这个命名空间中布满了泛型类型。和非泛型命名空间一样,这里包含了大量的类和接口容器(接口就不再赘述了),这些容器能容纳各种类型。
按照约定,泛型类型使用普通名字指定他们的占位符(当然一个泛型可以有无数个占位符),使用字母和字符串都可以,但是还是有通常的规则,T表示类型,Tkey表示键,Tvalue表示值。
许多泛型集合类(System.Collections.Generic中)都在System.Collections对应着一个非泛型副本。比如List<T> 对应ArrayList。
在实例化泛型类型时,必须带有new关键字和需要的构造函数参数。此外,需要指定在泛型类型定义中类型参数的替代类型。例如:
List<int> myint=new List<int>();
这样,这个 myint 将在其 List 中将定义时所有的占位符都替换为int。这样,我们就只能往myint中操作int类型了,而相应的内部也无需进行装箱和拆箱操作,因为里面的定义的参数类型均是只针对int的,而不是通用的object类型。
10.4 泛型的其他应用
虽大部分开发人员都使用基类库中已有的泛型类型,但是,我们当然可以构建自己的泛型成员和自定义的泛型类型。
(1)泛型方法
我们可以在普通的类和泛型类型中定义泛型方法:
例如:
1 |
static void Swap<T>( ref T a , ref T b) |
3 |
Console.Write(“The type of T is {0}”, typeof (T)); |
1 |
注意,泛型方法在方法名称后、参数列表前定义类型参数的(占位符)。前面说过,只要你需要,下面都是合法的: |
static test<T,K>(ref T a , ref K b){}
static test<hahaha>(ref hahaha a){}
在调用这个方法时(实例方法和静态方法),当且仅当泛型方法需要参数,而且参数也要有基于类型参数的类型时(且包括所有类型参数时),可以选择省略类型参数,因为编译器会基于参数推断类型参数。 如:
4 |
Swap< int >( ref b1, ref b2); |
但是若定义的泛型方法如下:
void Swap<T>(int a){}//带参数,但是没有类型参数
void Swap<T>(){}//不带参数
static void Swap<T,P>(ref T a, ref int b)//参数中没有完全包括所有类型参数
则调用时必须带上<T> 。
最后,实际上,定义的泛型方法的返回类型甚至也可以为T:
3 |
Console.WriteLine( typeof (T)); |
当然,似乎单独定义这样的方法意义不大。但是语法上是可以的。
最后,对于泛型方法的命名,它是否与现有成员方法冲突,是通过名称和类型参数的,而对于和成员变量,只通过名字来判断,例如,下面这两个显然不能同时定义的:
01 |
static void test1<w>() |
03 |
static void test1<t>() |
06 |
static void test1<w>() |
08 |
static void test1<t,w>() |
12 |
static void test1<T>() |
(2) 泛型结构和类
泛型结构和泛型类的定义方法是类似的。在他们内部,可以定义泛型方法(上面已经讲到)、泛型构造函数、泛型属性等各种泛型成员,当然也可以有普通成员。
01 |
Public struct point<T> |
10 |
Public void resetpoint() |
从上面可以看出,类型参数可以出现在字段数据定义、构造函数参数、属性定义中,只要是任何涉及到类型的,都可以用类型参数,只要有必要。注意上面使用了default关键字来获取占位符的默认值。其实,这个关键字也可以用于非泛型的时候,比如:
int a=default(int); // 虽然没这个必要,但是语法正确。
(3)自定义泛型集合
实际上,由于基类库提供了大量的允许我们创建类型安全且高效的容器,一般情况下不需要再构建自定义的集合类型。它的大部分好处就是可以添加有独特名字的方法,而不是使用通用的方法名称而已。
但是,自定义的泛型方法也是比较麻烦的,要实现这个集合的目的,还需要实现泛型的接口,比如一个自定义泛型集合,要foreach来遍历它,则应该实现IEnumerable<T>, 而由于此接口是继承了IEnumerable 接口,因此,实际上需要在自定义泛型集合中实现两个接口。另外,一般设计的自定义泛型集合都是专门为某种类型比如car类型或者兼容类型设计的,如果我们期待着的T应该是car类型,那么在编写方法时就有可能访问了car类型的一个成员,显然编译器是无法通过的,因为在用户使用过程中,可能传入非car类型(这是可能的)。当一个类型参数未被关联时,称该泛型类型是未绑定,依据设计,未绑定的类型参数被假定为仅仅是object成员,所以不可能有其他类的特有成员和成员变量。
在这里不再介绍自定义泛型集合的具体实现了。
(4)使用where关键字给泛型类型(或泛型方法)加约束条件
正如上面所说,开发人员编写自定义泛型集合,一般是为了对类型参数强制约束以构建更类型安全的容器中,通过where关键字,可以控制类型参数的各种特性(从而也就避免了上述的传入非期待的类型)。此时,若传入的类型参数不符合约束条件,将得到编译错误,而不是在运行期间,发生可能的异常抛出。
where 可以规定五个泛型约束:
where T:struct ,必须是值类型(并因为所有值类型都是结构体,所以就是约束必须是值类型)
where T:class ,非值类型,也就是引用类型
where T:new() ,必须有一个默认的构造函数,注意在多个约束组合时,这个放在最后面。注意,只有约束了这个条件,类中才能 new 一个T类型,否则会提示“变量类型“T”没有new() 约束,因此无法创建该类型的实例”。
where T:nameofbaseclass,必须是派生自指定的类(这个类或者子类)
where T:nameofinterface,必须实现指定的接口
where约束列表应该置于泛型类型的基类和列表之后。同时,where 可以约束包含多个类型参数的泛型类型参数。实际上,不仅仅泛型类型可以使用 where,泛型方法也可以使用它来约束:
例子:
Public class myclass<T,K>:
Mybase,ISomeInterface, where T:car,IEnumerable1, IEnumerable2 ,new() where K:struct
{}
意思就是myclass是一个泛型类,它继承了Mybase类,实现了ISomeInterface1、IEnumerable2接口,T必须是car类或其派生类,而且是必须有默认构造函数。而K必须是一个值类型。
疑问:如何约束只能是int类型呢?
注意,既然是加了约束条件,那么类型参数就可以认为是具有约束条件的类型了,在使用时,就可以认为他就是xx类型,从而访问xx的特有成员和变量,而不是仅仅的object类型了。
08 |
class cartest<T> where T:car, new () |
10 |
public static T tes= new T(); |
可惜的一点是,虽然我们告诉编译器我们传来的T是什么类型的,它其实并不能完全的理解。
1 )比如,即使我们指定T是car类型,而car确实有默认构造函数,如果不约束必须new,则还是无法实例化的。
06 |
class cartest<T> where T:car |
08 |
public static T tes= new T(); |
2 )另外,即使我们知道car有带有参数构造函数,还是不能在用T实例化时候使用有参构造函数进行初始化。
08 |
class cartest<T> where T:car, new () |
10 |
public static T tes= new T(4); |
3 )还有,虽然约束了T为某种类型(这里为car),但是无法直接用T来访问car的静态变量信息,例如:
05 |
public static void test2() |
08 |
class cartest<T> where T:car |
10 |
public void gettest () |
疑问:那么如何调用 T 的静态方法?
4 )最后一点,创建泛型方法时,如果应用类型参数上的任何C#运算符(加、减、乘、除、 == 等),将会收到编译器错误。编译器不允许向类型参数应用运算符。因为编译器不能保证一个未知的T重载了这些运算符而可以使用它们进行运算操作。
3 |
void T add(T arg1,T arg2) |
(5)创建泛型基类
泛型类是可以作为基类的,可以定义许多虚方法和抽象方法。但是,泛型类的派生类型必须遵守一些规则:
1)如果一个非泛型类扩展了一个泛型类,派生类必须为基类指定一个类型参数。
1 |
Public class parclass<T> |
2 |
Public class childclass:parclass< string >{} |
2)如果泛型基类定义了泛型虚方法或抽象方法,派生类必须使用上面使用的指定类型参数重写泛型方法(意思当然是可能要重写一个虚方法或者必须重写一个抽象方法喽):
1 |
Public class parclass<T> |
3 |
public virtual void print(T data){} |
5 |
Public class childclass:parclass< string > |
7 |
public override void print( string data){} |
3)如果派生类也是泛型,则它能够(可选的)重用类型占位符。不过要注意派生类型必须遵照基类中的任何约束,也就是泛型类如果派生另一个泛型类,只能是如下两种方式之一:
第一种,子类扩展父类,但是依然使用父类的类型参数,此时,子类的约束必须包括父类约束的前提下,再进行别的约束。
1 |
public class parclass<T> where T: DataTable |
5 |
public class childclass<T> : parclass<T> where T : DataTable, new () |
第二种,如果子类想实现父类,则必须指定一个类型参数给父类,自己还可以有新的类型参数。
1 |
public class parclass<T> |
4 |
public class childclass<TT>:parclass< string > |
因此,以下两种都是不对的:
01 |
public class parclass<T> |
03 |
public class childclass<T>:parclass<T> |
07 |
public class parclass<T> where T: struct |
09 |
public class childclass<T> : parclass<T> where T : DataTable |
11 |
总结一下,一个泛型基类,其他类(泛型或非泛型)要不就是实现它,在定义时就给出基类的类型参数,要不就被泛型类继续传递下去,不具体指定基类类型参数,但是这时候子泛型类必须有和基类相同的类型参数和具有基类约束规则的超集。<BR>正常情况下,构建泛型类层次结构的机会微乎其微,但是,只要遵守规则,机会也是有的。 |
(6)创建泛型接口
当然可以自由定义自己的泛型接口,带不带约束均可,只要满足接口定义和泛型类定义的约束规则,就没问题。
对于这种泛型接口被实现时,必须指定给接口确定的类型参数了,和泛型基类被实现是一样的。另外,对于泛型接口的传递,应该符合上面泛型类传递泛型基类的规则(书中没有论述,但规则是一致的)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.