5 数组和集合
上一章介绍了.NET中的字符串操作技术,下面继续介绍另一种常用的数据类型:数组和集合。同字符串一样,数组和集合也是Web开发中最重要的类型之一,用于表示一组性质相近的对象。
5.1 数组
数组能够按一定规律把相关的数据组织在一起,并通过“索引”或“下标”快速存取这些数据。本节首先介绍数组的基本概念,理解这些概念是学习使用数组的基础。
5.1.1 什么是数组
【本节示例参考:\示例代码\C05\Example_Array】
数组即一组数据,它把一系列数据组织在一起,成为一个可操作的整体。例如,当一个做事细心的妻子去超市买东西时,或许会事先列出一个清单:
(1)油;
(2)盐;
(3)酱;
(4)醋;
(5)毛毛熊;
(6)……
可以称这个清单为“需购物品”,它有规律地列出了其内部的数据,且其内部数据具有相同的性质。在程序语言中,可以称这样一个清单为数组:
//声明数组 String[] myStrArr =new String[5]{ "油", "盐", "酱", "醋", "毛毛熊" };
在数组中,其中的每一个元素对应排列次序下标。当使用其中的某个元素时,可以直接利用这个次序下标,具体如下:
1. //输出数组元素 2. for(int i=0;i<5;i++) 3. { 4. Page.Response.Write("myStrArr["+i+"]="+myStrArr[i]+"<br/>"); 5. }
将输出数组myStrArr中所有的元素,如图5.1所示。
图5.1 输出数组元素
说明
同C语言和大部分语言一样,C#的下标也是从0开始,而不是从1开始。
数组中的元素可以是任意的类型,如上面给出的示例,其中的每个元素都是字符串类型。除此之外,元素还可以是其他的基本数据类型,甚至又是一个数组。如果数组的元素又是数组,那么这个数组称为多维数组,下面是一个二维数组的例子:
//声明一个二维数组 String[,] myStrArr2 =new String[3,2]{ { "油", "盐" }, { "《围城》", "《晨露》" }, { "毛毛熊", "Snoopy" } };
在这个例子中,数组的第一维包含了三个元素,而每一个元素又是一个数组,分别包含了两个元素。此时称数组的秩为2;第一维的元素类型为数组,长度为3;第二维的元素类型为字符串,长度为2。
通过这个具体的例子,读者可以思考“秩”、“维”、“元素类型”,以及“长度”的概念。同一维数组一样,多维数组中的每一个元素,也可以通过下标方式来引用,如:
myStrArr[0]指一维数组{“油”,“盐”}。
myStrArr[0,0]指字符串“油”。
下面的代码,可以输出myStrArr2中的所有元素。
代码5-1 输出数组元素:Default.aspx.cs
1. //输出二维数组元素 2. for(int i=0;i<3;i++) 3. { 4. Page.Response.Write("--myStrArr2["+i+"]<br/>"); 5. for(int j=0;j<2;j++) 6. { 7. Page.Response.Write("----myStrArr2["+i+","+j+"]="+myStrArr2[i,j]+"<br/>"); 8. } 9. }
输出结果如图5.2所示。
图5.2 分层输出二维数组
5.1.2 创建数组
【本节示例参考:\示例代码\C05\Array_Create】
在C#中,使用如下语法创建一个数组。
1.一维数组
data type[]arr name=new data type[int length]
这种方式定义一个元素数据类型为data type、长度为length的数组arr name,例如:
int[]myIntArr=new int[100]; //定义一个长度为100的int数组 string[]mystringArr=new string[100]; //定义一个长度为100的string数组 object[]myObjectArr=new object[100]; //定义一个长度为100的object数组
其中,数据类型data type既可以是常用数据类型(如int、float等),也可以是对象(如String、StringBuilder等)。
data type[]arr name=new data type[]{item1,item2,…,itemn}
这种方式定义一个元素数据类型为data type,并通过“=”运算符进行赋值,其初始值为所给出的元素{item1,item2,…,itemn}的个数,例如:
int[]myIntArr2=new int[]{1,2,3}; //定义一个int数组,长度为3 string[]mystringArr2=new string[]{"油","盐"}; //定义一个string数组,长度为2
在这种定义下,不必给出数组的长度定义,数组的长度自动设置,为所给出的元素{item1,item2,…,itemn}的个数。即下面的两种定义完全相同:
int[] myIntArr2=new int[]{1,2,3}; int[] myIntArr2=new int[3]{1,2,3};
2.多维数组
data type[,…,]arr name=new data type[int length1,int length2,…,int lengthn]
这种方式定义一个元素数据类型为data type,秩为n,各维长度分别为length1,length2,…,lengthn的数组arr name,例如:
int[,]myIntArr=new int[10,100]; //定义一个10*100的二维int数组 string[,,]mystringArr=new string[2,2,3]; //定义一个2*2*3的三维string数组
这里就定义了两个多维数组。也可以用下面的方法进行多维数组的定义:
data_type[,…,] arr_name = new data_type[,…,]] { {item1, item2, … ,itemn} … }
例如:
int[,]myIntArr2=new int[,]{{1,2,3},{-1,-2,-3}}; //2*3的二维int数组 string[,]mystringArr2=new string[,]{{"油","盐"},{"《围城》","《晨露》"}}; //2*2的二维string数组
同一维数组一样,在这种定义下,可以不必给出各维的长度定义,各维长度根据所给出的赋值元素自动确定。
3.交错数组
C#支持各个维度长度不同的多维数组,称为交错数组,也称为“数组的数组”。交错数组的定义如下:
data type[][]…arr name=new data type[int length1][int length2]…
这个定义和定义多维数组非常类似,区别在于,交错数组必须单独初始化交错数组每一维中的元素。例如,下面定义一个第一维长度为3的交错数组:
1. int[][]myJaggedArray=new int[3][]; 2. myJaggedArray[0]=new int[5]; 3. myJaggedArray[1]=new int[4]; 4. myJaggedArray[2]=new int[2];
这个交错数组myJaggedArray,每个元素都是一个一维整数数组。第一个元素是由5个整数组成的数组,第二个是由4个整数组成的数组,而第三个是由2个整数组成的数组。
data_type[][]…arr_name=new data_type[][]… { new data_type[]{item1,…,new data_type[]itemn} … }
这种方式在声明数组的同时进行初始化,同样可以不指定各维的长度,例如:
1. int[][]myJaggedArray=new int[][] 2. { 3. new int[]{1,3,5,7,9}, 4. new int[]{0,2,4,6}, 5. new int[]{11,22} 6. };
5.1.3 数组基类Array
System.Array类是所有.NET中数组的基类,提供创建、操作、搜索和排序数组的方法,其属性和方法如图5.3所示。
图5.3 System.Array类
Array类常用属性和方法的简单说明,如表5.1所示。
表5.1 Array类常用属性/方法说明
5.1.4 访问数组元素
【本节示例参考:\示例代码\C05\Array_AccessItem】
访问数组的元素包括读取或设置某个元素的值,最基本的方法是通过下标定位元素,另外还可以使用GetValue/SetValue方法。
1.通过下标定位元素
C#中数组对其中的元素进行排序,并从0开始计数,这样每一个元素都会有一个唯一的下标,通过这个下标,就可以定位唯一的一个元素。下面通过示例来说明。
(1)一维数组:
string[]myStrArr={"油", "盐", "酱", "醋", "毛毛熊"};
这里,myStrArr[0]=“油”;myStrArr[4]=“毛毛熊”。如果试图访问超过下标范围的数据,则会出现如下异常:
System.IndexOutOfRangeException: 索引超出了数组界限
(2)多维数组:
string[,] myStrArr2={{"油","盐"},{"《围城》","《晨露》"},{"毛毛熊","Snoopy"}};
定义之后,myStrArr2[0,0]=“油”;myStrArr2[2,1]=“Snoopy”。
(3)交错数组:
1. int[][]myJaggedArray=new int[][] 2. { 3. new int[]{1,3,5,7,9}, 4. new int[]{0,2,4,6}, 5. new int[]{11,22} 6. };
定义之后则有:myJaggedArray[0][0]=1;myJaggedArray[1][1]=2;myJaggedArray[2][1]=22。
下面的代码可以循环输出所有的交错数组元素。
代码5-2 输出交错数组元素:Default.aspx.cs
1. for(int i=myJaggedArray.GetLowerBound(0);i<=myJaggedArray.GetUpperBound(0);i++) 2. { 3. Console.WriteLine("item{0}",i); 4. for(int j=myJaggedArray[i].GetLowerBound(0);j<=myJaggedArray[i].GetUpperBound(0);j++) 5. { 6. Console.WriteLine(" item{0}{1}:{2}",i,j,myJaggedArray[i][j]); 7. } 8. }
2.使用GetValue/SetValue
GetValue方法定义如下:
public object GetValue(params int[]indices);
其中,多个int型参数indices的含义为下标。方法返回一个object对象,这是C#中所有对象的基类,使用多态性,它可以指向所有的C#对象。下面的代码使用GetValue方法,循环输出一个二维数组所有元素。
代码5-3 使用GetValue输出二维数组元素示例:Default.aspx.cs
1. //定义二维数组 2. string[,]myStrArr2=new string[,]{{"油","盐"},{"《围城》","《晨露》"},{"毛毛熊","Snoopy"}}; 3. //循环输出 4. for(int i=myStrArr2.GetLowerBound(0);i<=myStrArr2.GetUpperBound(0);i++) 5. { 6. Console.WriteLine("item{0}",i); 7. for(int j=myStrArr2.GetLowerBound(1);j<=myStrArr2.GetUpperBound(1);j++) 8. { 9. Console.WriteLine(" item{0}{1}:{2}",i,j,myStrArr2.GetValue(i,j)); 10. } 11. }
SetValue的功能为数组的某个元素赋值,其定义及参数表同GetValue相似,不作赘述。
5.1.5 转化元素类型
【本节示例参考:\示例代码\C05\ Array_ConvertAll】
定义数组时,需要为其中的元素指定数据类型。有时候,在应用中需要重新转化元素的类型,这可以使用Array对象的CovertAll方法实现:
public static TOutput[] ConvertAll<TInput,TOutput> (TInput[] array,Converter <TInput,TOutput> converter)
其中,类型参数TInput为源数组元素的类型,TOutput为目标数组元素的类型。参数array为要转换为目标类型的从零开始的一维Array,converter为一个Converter对象,用于将每个元素从一种类型转换为另一种类型。方法的返回值是目标类型的数组,包含从源数组转换而来的元素。
Convert对象表示将对象从一种类型转换为另一种类型的方法,其定义为:
public delegate TOutput Converter<TInput,TOutput>( TInput input)
其中,类型参数TInput为要转换的对象的类型,TOutput为要将输入对象转换到的类型。参数input为要转换的对象。方法的返回值为TOutput,它表示已转换的TInput。
下面的代码示例将一个元素类型为int的数组转化为string类型。
代码5-4 使用ConvertAll转化数据元素类型:Default.aspx.cs
1. protected void Page_Load(object sender,EventArgs e) 2. { 3. //定义一个整数类型的数组,并输出 4. int[]src={1,2,3}; 5. Page.Response.Write("<br/>"); 6. foreach(int i in src) 7. { 8. Page.Response.Write(i+":"+i.GetType()+"<br/>"); 9. } 10. 11. //将整数数组转化为字符串类型,并输出 12. string[]desc=Array.ConvertAll(src, 13. new Converter<int,string>(IntToString)); 14. Page.Response.Write("<br/>"); 15. foreach(string str in desc) 16. { 17. Page.Response.Write(str+":"+str.GetType()+"<br/>"); 18. } 19. } 20. 21. ///<summary> 22. ///Converter委托 23. ///</summary> 24. ///<param name="i">整数类型的数据</param> 25. ///<returns>由输入转化而成的字符串类型数据</returns> 26. public static string IntToString(int i) 27. { 28. return i.ToString(); 29. }
代码首先在第3~9行定义了一个整数类型的数组src,并输出其中的元素和数据类型;
第11~13行利用Array类的ConvertAll方法将src转化为字符串类型,方法的第二个参数中使用了一个委托IntToString,其实现在第21~29行。这个委托功能简单,将一个输入的整数参数转化为字符串,并返回。
第14~19行将转化后的字符串数组输出。程序运行结果如图5.4所示。
图5.4 使用ConvertAll方法转化数组元素类型
5.1.6 遍历数组元素
【本节示例参考:\示例代码\C05\ Array_Traverse】
遍历数组是指访问数组中的全部元素一次并且仅一次。可以在遍历的过程中完成许多操作,如查找等。有两种方式可以遍历整个数组,具体如下。
1.使用GetLowerBound/GetUpperBound方法
GetLowerBound方法可以获取数组某一维上的最低下标,而GetUpperBound则可获取其最高下标,利用这两个参数和for语句,可以实现数组的遍历。
public int GetLowerBound (int dimension) public int GetUpperBound (int dimension)
其中,参数dimension为需要获取上下标的数组维度。
下面的示例实现了对二维数组的遍历。
代码5-5 利用for语句遍历数组示例:Default.aspx.cs
1. //定义二维数组 2. string[,]myStrArr2=new string[,]{{"油","盐"},{"《围城》","《晨露》"},{"毛毛熊","Snoopy"}}; 3. //遍历 4. for(int i=myStrArr2.GetLowerBound(0);i<=myStrArr2.GetUpperBound(0);i++) 5. { 6. for(int j=myStrArr2.GetLowerBound(1);j<=myStrArr2.GetUpperBound(1);j++) 7. { 8. //处理每一个元素 9. } 10. }
2.使用foreach
还可以使用更为简便的方法来实现数组的遍历,那就是foreach关键字,这种方法对于处理高维数组尤其方便。foreach语句格式如下:
foreach (data_typt item_name in arr_name) { //处理每一个元素 }
注意
foreach语句获取的元素是最深层的元素,因此,无论处理几维的数组,使用一层的foreach循环就可以了。
例如,下面的代码实现同样的二维数组遍历。
代码5-6 利用foreach遍历数组示例:Default.aspx.cs
1. //定义二维数组 2. string[,]myStrArr2=new string[,]{{"油","盐"},{"《围城》","《晨露》"},{"毛毛熊","Snoopy"}}; 3. //遍历 4. foreach(string item in myStrArr2) 5. { 6. { 7. //处理每一个元素 8. } 9. }
5.1.7 排序数组元素
【本节示例参考:\示例代码\C05\ Array_Sort】
对数组进行排序是指按照一定的排序规则,如递增或递减规则,重新排列数组中的所有元素。可以使用Array类的Sort方法完成这个功能。Sort方法有多种重载方式,常用的形式如下:
public static void Sort(Array array);
其中,参数array为待排序的数组。下面的示例首先定义了一个数组,含有元素{5,4,3,2,1},然后利用Sort方法对其排序。
代码5-7 利用Sort排序数组示例:Default.aspx.cs
1. ///<summary> 2. /// 利用Sort方法排序数组 3. ///</summary> 4. public void TestSort() 5. { 6. int[]myArr={5,4,3,2,1}; //定义数组 7. 8. //输出原始数组:原始数组:5->4->3->2->1-> 9. Page.Response.Write("原始数组:"); 10. for(int i=0;i<myArr.Length;i++) 11. Page.Response.Write(myArr[i]+"->"); 12. 13. Array.Sort(myArr); //对数组排序 14. 15. Page.Response.Write("<br/>"); 16. 17. //并输出排序后的数组:1->2->3->4->5-> 18. Page.Response.Write("排序以后数组:"); 19. for(int i=0;i<myArr.Length;i++) 20. Page.Response.Write(myArr[i]+"->"); 21. }
有时候需要进行所谓的关键字排序,例如,有两个数组arrSid和arrSname,分别代表一组学生的学号和姓名,如果想要根据学号顺序输出姓名,或反之,都需要使用数组的排序操作,那么,如何把这两个数组联系在一起排序呢?这时就可以使用Sort的下面这种形式进行关键字排序。
public static void Sort(Array keys, Array items);
其中,参数keys代表关键字数组,而items代表另一个数组。利用Sort,下面的代码可实现上述需求。
代码5-8 利用Sort实现数组多关键字排序示例:Default.aspx.cs
1. ///<summary> 2. /// 利用Sort实现数组多关键字排序 3. ///</summary> 4. public void TestSortMultiKey() 5. { 6. //定义数组 7. int[]arrSid={5,4,3,2,1}; 8. string[]arrSname={"张三","李四","王五","麻子","淘气"}; 9. 10. //输出原始数组:原始数组:张三(5)->李四(4)->王五(3)->麻子(2)->淘气(1)-> 11. Page.Response.Write("原始数组:"); 12. for(int i=0;i<arrSid.Length;i++) 13. Page.Response.Write(arrSname[i]+"("+arrSid[i]+"->"); 14. 15. //根据学号关键字排序 16. Array.Sort(arrSid,arrSname); 17. Page.Response.Write("<br/>"); 18. 19. //并输出排序后的数组:淘气(1)->麻子(2)->王五(3)->李四(4)->张三(5) 20. Page.Response.Write("排序以后数组:"); 21. for(int i=0;i<arrSid.Length;i++) 22. Page.Response.Write(arrSname[i]+"("+arrSid[i]+"->"); 23. }
示例非常简单,输出已经在注释中给出,因此不作详细说明。
5.1.8 查找数组元素
【本节示例参考:\示例代码\C05\ Array_Search】
在数组中查找元素,可以有两种解释:一是从整个数组中寻找到与给定值相同的元素,可以使用Array类的BinarySearch方法完成这个功能;二是判断数组中是否含有一个特定的元素,可以用Contains方法实现。
1.BinarySearch方法
BinarySearch使用二进制搜索算法在一维的排序Array中搜索值,注意必须是已经排序的数组。如果找到给定的值,则返回其下标;否则,返回一个负整数。其常用形式如下:
public static int BinarySearch(Array array,object value);
其中,参数array为待搜索的数组,value为要寻找的元素值。下面的示例首先定义了一个数组,含有元素{5,4,3,2,1},然后利用BinarySearch方法返回其中的元素3的下标(2)。
代码5-9 利用BinarySearch搜索数组元素示例:Default.aspx.cs
1. ///<summary> 2. /// 利用BinarySearch二分搜索 3. ///</summary> 4. void TestBinarySearch() 5. { 6. //定义数组 7. int[]myArr={5,4,3,2,1}; 8. 9. //利用Sort排序 10. Array.Sort(myArr); 11. 12. //利用BinarySearch二分搜索 13. int target=3; 14. int result=Array.BinarySearch(myArr,target);//2 15. Page.Response.Write(target+"的下标为"+result+"<br/>"); //2 16. }
2.Contains方法
Contains方法可以确定某个特定值是否包含在数组中,返回一个bool值。Array类的这个方法实际上是对IList接口中方法的实现,其常用形式为:
bool IList.Contains(object value);
其中,参数value代表所要验证的元素值,下面的示例判断学生数组arrSname中是否包含“王五”。
代码5-10 利用Contains判断数组是否包含某个元素:Default.aspx.cs
1. ///<summary> 2. /// 利用Contains方法检查是否包含某个元素 3. ///</summary> 4. void TestContains() 5. { 6. //定义数组 7. string[]arrSname={"张三","李四","王五","麻子","淘气"}; 8. 9. //判断是否含有某值 10. string target="王五"; 11. bool result=((System.Collections.IList)arrSname).Contains(target); 12. Page.Response.Write("是否包含"+target+"="+result+"<br/>"); //true 13. }
可以看到,在使用Contains方法时,需要首先将数组转换为IList(队列集合)对象。这是因为,本质上,数组是一种特殊的集合对象,因此可以把它转换为一个集合对象。对于集合,将在下一章中对其进行详细的讨论。
5.1.9 反转数组元素
【本节示例参考:\示例代码\C05\ Array_Reverse】
反转数组是指将一维数组中的全部或部分元素的顺序,按照其逆序重新排列。可以使用Array类的Reverse静态方法完成这个功能。其常用的形式为:
public static int Reverse(Array array); public static int Reverse(Array array,int index, int length);
其中,参数index指定所要反转元素的起始下标,而length指定所要反转的元素个数。
第一个重载形式可以反转整个数组元素,参数array为待反转的数组。下面的示例首先定义了一个数组,含有元素{5,4,3,2,1},然后利用Reverse方法进行反转。
代码5-11 利用Reverse反转数组示例:Default.aspx.cs
1. ///<summary> 2. /// 利用Reverse方法反转数组 3. ///</summary> 4. public void TestReverse() 5. { 6. //定义数组 7. int[]myArr={5,4,3,2,1}; 8. 9. //输出原始数组:原始数组:5->4->3->2->1-> 10. Page.Response.Write("原始数组:"); 11. for(int i=0;i<myArr.Length;i++) 12. Page.Response.Write(myArr[i]+","); 13. 14. Page.Response.Write("<br/>"); 15. 16. //对数组反转 17. Array.Reverse(myArr); 18. 19. //并输出反转后的数组:1->2->3->4->5-> 20. Page.Response.Write("反转以后数组:"); 21. for(int i=0;i<myArr.Length;i++) 22. Page.Response.Write(myArr[i]+","); 23. }
5.1.10 复制数组
【本节示例参考:\示例代码\C05\ Array_Copy】
复制数组可以得到一个和原数组完全一样的新数组,可以用Array的Copy或CopyTo方法来实现。
1.Copy方法
在使用Copy方法进行数组复制操作之前,必须首先为新的数组分配空间,然后再通过复制操作向新的数组空间中填入元素值。静态方法Copy的常用重载形式为:
public static void Copy(Array sourceArray,Array destinationArray,int length);
其中,参数sourceArray为源数组,destinationArray为目标数组,而length为要复制的元素数目,默认的复制操作从第一个元素开始。下面的示例首先定义了一个数组,含有元素{5,4,3,2,1},然后利用Copy方法获取一个新的数组,包含了源数组的前3项。
代码5-12 利用Copy复制数组示例:Default.aspx.cs
1. ///<summary> 2. /// 利用Copy静态方法复制数组 3. ///</summary> 4. public void TestCopy() 5. { 6. //定义数组 7. int[]myArr={5,4,3,2,1}; 8. 9. //输出原始数组:原始数组:5,4,3,2,1, 10. Page.Response.Write("原始数组:"); 11. for(int i=0;i<myArr.Length;i++) 12. Page.Response.Write(myArr[i]+","); 13. Page.Response.Write("<br/>"); 14. 15. //复制数组 16. int[]newArr=new int[3]; 17. Array.Copy(myArr,newArr,3); 18. 19. //并输出反复制的数组:5,4,3, 20. Page.Response.Write("复制数组:"); 21. for(int i=0;i<newArr.Length;i++) 22. Page.Response.Write(myArr[i]+","); 23. }
注意
在使用Copy进行复制数组之前,必须要首先定义一个新的数组,并对其分配空间。另外,还需保证所分配的空间足够容纳所要复制的元素。否则在复制时将出现异常:“System.ArgumentException:目标数组的长度不够”。
2.CopyTo方法
CopyTo和Copy方法的功能类似,但它是一个实例方法,即需要在数组对象上引用。另外,它只能复制源数组所有的元素,并可以控制元素在目标数组中存放的起始位置。其常用重载形式为:
public virtual void CopyTo( Array array, int index);
其中,参数array代表所要复制的目标数组,index则表示开始复制的元素下标。下面的代码实现对源数组的复制,并从第3个位置开始存放。
代码5-13 利用CopyTo复制数组示例:Default.aspx.cs
1. ///<summary> 2. /// 利用CopyTo静态方法复制数组 3. ///</summary> 4. public void TestCopyTo() 5. { 6. //定义数组 7. int[]myArr={5,4,3,2,1}; 8. 9. //输出原始数组:原始数组:5,4,3,2,1, 10. Page.Response.Write("原始数组:"); 11. for(int i=0;i<myArr.Length;i++) 12. Page.Response.Write(myArr[i]+","); 13. Page.Response.Write("<br/>"); 14. 15. //复制数组 16. int[]newArr=new int[7]; 17. myArr.CopyTo(newArr,2); 18. 19. //并输出复制的数组:0,0,5,4,3,2,1, 20. Page.Response.Write("复制数组:"); 21. for(int i=0;i<newArr.Length;i++) 22. Page.Response.Write(newArr[i]+","); 23. }
另外,从输出结果还可以看到,在初始化int型数组时,其默认值为0。
5.2 集合
上面介绍的数组常常用来实现静态的操作,即不改变其空间大小,如查找、遍历等。数组也可以实现动态的操作,如插入、删除等,但不推荐使用,而应尽量使用本节介绍的集合来代替。
5.2.1 什么是集合
集合是指一组类似的对象。在.NET中,任意类型的对象都可以放入一个集合中。.NET中的集合类存在于System.Collections命名空间中,其常用的类和结构如图5.5所示。
图5.5 System.Collections命名空间常用类和结构
从图5.5中可以看出,System.Collections空间中的集合种类众多,主要包括如下。
1.一般集合
一般集合是常见的集合数据结构,包括哈希表、队列、堆栈、字典和列表等。对于这些集合的详细含义,读者可参考数据结构方面的资料,本书不作详细介绍,简单说明如下:
(1)列表(ArrayList):一个一维的动态数组,可以装载一组相似的数据元素。
(2)队列(Quene):先进先出的列表。
(3)堆栈(Stack):先进后出的列表。
(4)哈希表(Hashtable):集合中的每个元素都是一个<键(key),值(value)>对的列表。
(5)字典(DictionaryEntry):一个<键(key),值(value)>对。
2.专用集合
专用集合是具有特定用途的集合,如StringCollection类,其元素只能为String对象。
3.位集合
位集合的元素为位标志,每个元素都是一位,而不是一个对象,常用于操作数只包含1、0的集合。
5.2.2 列表类ArrayList
System.Collections.ArrayList类实现了可变大小的一维数组,其常用属性和方法如图5.6所示。
图5.6 ArrayList类的属性和方法
ArrayList类常用属性和方法的简单说明,如表5.2所示。
表5.2 ArrayList类常用属性/方法说明
下面各节,将详细介绍其中最常用的方法。
5.2.3 创建列表
【本节示例参考:\示例代码\C05\ArrayList_Create】
利用ArrayList的构造函数来创建一个新的列表,常用的形式如下:
public ArrayList(); public ArrayList(int capacity);
参数capacity可以指定所创建列表的初始容量。如果不指定,则初始容量为.NET的默认值16。下面的代码创建了两个列表对象:
ArrayList arr1=new ArrayList(); ArrayList arr2=new ArrayList(100);
其中,arr1的初始容量为16,arr2为100。目前,两者里面都是空的,没有任何元素。随着操作的进行,当列表中的元素达到其最大容量时,列表将自动将其容量增加1倍。另外,如果想要使用ArrayList,首先需要在代码头部引入命名空间:
using System.Collections;
5.2.4 遍历列表
【本节示例参考:\示例代码\C05\ArrayList_Traverse】
1.使用foreach语句
遍历列表是指访问列表中的所有元素一遍,可以使用foreach语句完成这个功能。下例使用foreach输出列表arr1中的所有元素。
代码5-14 使用foreach遍历列表示例:Default.aspx.cs
1. ///<summary> 2. /// 利用Foreach语句遍历集合 3. ///</summary> 4. void TestForeach() 5. { 6. ArrayList arr1=new ArrayList(); 7. 8. //循环添加元素0~9 9. for(int i=0;i<10;i++) 10. arr1.Add(i); 11. 12. //使用foreach遍历数组,输出所有元素 13. foreach(object item in arr1) 14. { 15. Page.Response.Write(item+"<br/>"); 16. } 17. }
2.使用GetEnumerator方法
除了foreach之外,还可以使用ArrayList的GetEnumerator方法实现列表的遍历。其形式为:
public virtual IEnumerator GetEnumerator();
该方法返回整个ArrayList的枚举对象IEnumerator。这个对象可以得到一个集合对象的所有元素,其最主要的属性为Current,用于获取集合中的当前元素。
主要的方法包括:
MoveNext:将枚举推进到集合的下一个元素。在传递到集合的末尾之后,枚举数放在集合中最后一个元素后面,且调用MoveNext会返回false。
Reset:将枚举设置为其初始位置,该位置位于集合中第一个元素之前。
下面的代码使用GetEnumerator实现上面foreach同样的列表遍历功能。
代码5-15 使用GetEnumerator遍历列表示例:Default.aspx.cs
1. ///<summary> 2. /// 利用GetEnumerator遍历数组 3. ///</summary> 4. void TestGetEnumerator() 5. { 6. ArrayList arr1=new ArrayList(); 7. 8. //循环添加元素0~9 9. for(int i=0;i<10;i++) 10. arr1.Add(i); 11. 12. //使用GetEnumerator遍历数组 13. System.Collections.IEnumerator enm=arr1.GetEnumerator(); 14. while(enm.MoveNext()) 15. { 16. Page.Response.Write(enm.Current+"<br/>"); 17. } 18. }
注意
最初,枚举数被定位于集合中第一个元素的前面。此时,调用Current会引发异常。因此在读取Current的值之前,必须调用MoveNext将枚举数提前到集合的第一个元素。
5.2.5 添加元素
【本节示例参考:\示例代码\C05\ArrayList_Add】
可以通过ArrayList的Add和AddRange方法,实现向一个列表中添加数据。两者的区别在于:Add一次只能添加一个元素,而AddRange一次可以添加多个元素,这多个元素需要放在一个集合或数组中。两者常用的形式如下:
public int Add(object value); public void AddRange(ICollection c);
下面的示例中,首先定义了一个列表arr1,然后使用Add方法,向arr1中添加了两个元素,其中第一个为字符串对象“Hello”,第二个为一个整数对象1。
然后分别定义了两个列表arr2、arr3,并分别使用Add和AddRange方法试图将arr1中的所有数据都添加到arr2、arr3中。从结果中可以看出,只有使用AddRange才能实现这个目的,而使用Add方法则可以得到一个二维数组,第一维的元素为arr1。
代码5-16 向ArrayList中添加元素示例:Default.aspx.cs
1. ArrayList arr1=new ArrayList(); 2. 3. //向arr1中添加一个字符串对象“Hello” 4. object item=new object(); 5. item="Hello"; 6. arr1.Add(item); //arr1:{"Hello"} 7. //向arr1中添加一个整数对象1 8. item=1; 9. arr1.Add(item); //arr1:{"Hello",1} 10. 11. //向另一个列表中添加arr1中的所有元素 12. ArrayList arr2=new ArrayList(); 13. arr2.Add(arr1); //arr2只有一个元素:{arr1}={{“Hello",1}} 14. 15. ArrayList arr3=new ArrayList(); 16. arr3.AddRange(arr1); //arr3:{"Hello",1}
Add和AddRange方法只能将元素添加到列表的末尾,如果想要在列表的任意位置添加元素,则需要使用Insert方法。
5.2.6 插入元素
【本节示例参考:\示例代码\C05\ArrayList_Insert】
如前面所述,使用ArrayList的Insert和InsertRange方法,可以实现向一个列表中的任意位置添加数据。这两者的区别同Add与AddRange的区别类似,Insert一次只能添加一个元素,而InsertRange一次可以添加某个集合中的多个元素。两者常用的形式分别如下:
public int Insert(int index, object value); public void InsertRange(int index, ICollection c);
其中,参数index指定元素所要插入的位置,从0开始索引。下面的示例中,首先定义了一个列表arr1,然后使用Add方法,向arr1中添加了两个元素,其中第1个为字符串对象“Hello”,第2个为一个整数对象1。然后,在两者之间插入一个整数型数组元素{1,2,3}。
代码5-17 向ArrayList中插入元素示例:Default.aspx.cs
1. ArrayList arr1=new ArrayList(); 2. 3. //向arr1中添加一个字符串对象“Hello” 4. object item=new object(); 5. item="Hello"; 6. arr1.Add(item); //arr1:{"Hello"} 7. //向arr1中添加一个整数对象1 8. item=1; 9. arr1.Add(item); //arr1:{"Hello",1} 10. 11. //插入一个数组元素arr2 12. int[]arr2=new int[]{1,2,3}; 13. arr1.Insert(1,arr2);
使用Insert方法把arr2插入在位置1后,arr1的结果如图5.7所示。可以看出,arr1的长度为3,下标1处的元素为数组arr2,里面又包括了3个整数元素。
图5.7 使用Insert后的arr1结果
如果把代码第13行的Insert改为InsertRange,则将会把数组中的所有元素插入到arr1中,而不是数组本身,结果将如图5.8所示。
图5.8 使用InsertRange后的arr1结果
5.2.7 删除元素
【本节示例参考:\示例代码\C05\ArrayList_Delete】
ArrayList中支持删除元素的方法分别如下:
public void Remove(object obj);
该方法用于删除数组中特定对象obj的第一个匹配项。参数obj为要从ArrayList移除的Object。
public void RemoveAt(int index);
该方法用于移除ArrayList的指定索引处的元素。参数index为要移除的元素的从0开始的索引。
public void RemoveRange(int index,int count);
该方法用于从ArrayList中移除一定范围的元素。参数index为要移除元素的起始索引(从0开始计数),参数count为要移除的元素数。
下面的示例中,首先定义了一个列表arr1,使用Add方法添加进10个元素(整数0~9),然后分别使用上面的3种方法,分别删除掉元素3、元素5,以及元素{7, 8, 9}。
代码5-18 从ArrayList中删除元素示例:Default.aspx.cs
1. ArrayList arr1=new ArrayList(); 2. 3. //循环添加元素1~9 4. for(int i=0;i<10;i++) 5. arr1.Add(i); 6. 7. //使用Remove(),删除掉3 8. arr1.Remove(3); 9. 10. //使用RemoveAt(),删除掉5,注意:此时,元素5的下标为? 11. arr1.RemoveAt(4); 12. 13. //使用RemoveRange(),一次删除掉7、8、9,注意,元素7的下标为? 14. arr1.RemoveRange(5,3);
5.2.8 简单排序
【本节示例参考:\示例代码\C05\ArrayList_SimpleSort】
对集合元素进行排序也是非常重要的操作之一,许多进一步的操作可以在排序的基础上进行,例如二分查找等。ArrayList使用Sort方法来实现排序功能,其常用形式如下:
public virtual void Sort();
该形式可以对整个ArrayList中的元素进行排序。
下例简单使用了无参数的Sort方法,对一个列表中的元素进行排序。这时,默认的排序策略为非递减排序。
代码5-19 使用Sort方法对列表排序示例:Default.aspx.cs
1. ///<summary> 2. /// 利用Sort方法进行排序 3. ///</summary> 4. public void TestSort() 5. { 6. ArrayList arr1=new ArrayList(); 7. 8. //循环添加元素5~1 9. for(int i=5;i>0;i--) //arr1:{5,4,3,2,1} 10. arr1.Add(i); 11. 12. //使用Sort(),实现简单的非递减排序 13. arr1.Sort(); //arr1:{1,2,3,4,5} 14. }
5.2.9 复杂排序
【本节示例参考:\示例代码\C05\ArrayList_ComplexSort】
有时需要进行更为复杂的排序操作。例如,利用逆向比较策略(即1>2策略)进行排序等。这时需要首先利用IComparer接口实现一个具体的比较策略,然后再利用Sort方法进行排序,所用到的Sort方法形式如下:
public virtual void Sort(IComparer comparer);
该方法使用指定的比较策略对整个ArrayList中的元素进行排序。其中,参数comparer为比较元素时要使用的比较策略,是对接口IComparer的实现。
下例中,首先利用IComparer接口方法,实现一个逆向比较策略类。IComparer中包含一个Compare接口,需要对这个接口进行实现,返回一个整数值来表示两个待比较对象x、y的大小关系。正数表示x>y;0表示x= =y;负数表示x<y。
代码5-20 使用Compare方法实现逆比较示例:Default.aspx.cs
1. public class myReverserClass:IComparer 2. { 3. //实现IComparer接口的Compare方法,实现逆比较 4. int IComparer.Compare(Object x,Object y) 5. { 6. if(Convert.ToInt32(x)>Convert.ToInt32(y)) 7. return-1; 8. else if(Convert.ToInt32(x)==Convert.ToInt32(y)) 9. return 0; 10. else 11. return 1; 12. } 13. }
然后,便可以使用这个比较器,结合Sort方法进行逆排序,如下所示:
1. ArrayList arr1=new ArrayList(); 2. 3. //循环添加元素1~5 4. for(int i=1;i<6;i++) //arr1:{1,2,3,4,5} 5. arr1.Add(i); 6. 7. //使用逆比较策略,实现逆排序 8. IComparer myComparer=new myCompareReverse(); 9. arr1.Sort(myComparer); //arr1:{5,4,3,2,1}
另外,当需要对数组中的某一部分进行排序时,可以使用Sort的如下形式:
public virtual void Sort(int index,int count,IComparer comparer);
这里使用指定的比较策略对部分ArrayList中的元素进行排序。参数index为要排序的起始索引,count为要排序的范围的长度,comparer为比较元素时要使用的比较策略,是对接口IComparer的实现。
5.2.10 查找元素
【本节示例参考:\示例代码\C05\ArrayList_Search】
在集合中对特定元素的查找也是常用的操作之一,ArrayList提供了二分查找的方法BinarySearch,可以在O(logn)时间复杂度内完成查找,其常用形式如下:
public virtual int BinarySearch(object value);
在整个已排序的ArrayList中搜索元素,并返回该元素从0开始的索引。
代码5-21首先使用默认的非递增简单排序方法对一个列表中的元素进行排序,然后使用BinarySearch方法搜索其中的特定元素,并输出其索引。
代码5-21 使用BinarySearch方法查找元素示例:Default.aspx.cs
1. ArrayList arr1=new ArrayList(); 2. 3. //循环添加元素10~1 4. for(int i=0;i<10;i++) 5. arr1.Add(10-i); 6. 7. //使用BinarySearch,查找元素7,并输出 8. arr1.Sort(); //首先需要进行排序 9. int idx=arr1.BinarySearch(7); 10. Page.Response.Write("arr["+idx+"]=7");//arr[6]=7
如果使用指定的排序策略对集合中的元素进行排序之后,相应地,也可以使用同样的排序策略,结合BinarySearch方法实现元素的查找。这时,其形式为:
public virtual int BinarySearch(object value,IComparer comparer);
此时,将使用指定的比较器在整个已排序的ArrayList中搜索元素,并返回该元素从0开始的索引。参数value为要查找的元素,而Icomparer为指定的比较策略,可参考5.2.9节相关内容。
5.3 队列
上一节介绍了集合中的列表类ArrayList,下面介绍一个特殊的列表——队列。
5.3.1 什么是队列
队列实际上是一种特殊的列表。它对列表的操作进行了限制,要求列表中的元素必须满足先进先出的原则,这类似于现实生活中的排队,如图5.9所示。
图5.9 队列示意图
说明
队列及后面所介绍的堆栈等,都是非常重要的数据结构,掌握它们的思想及典型应用,在程序设计中具有重要的作用。但是,本书将不从程序设计的层次讨论Queue类的应用,而仅从集合操作的层次进行讨论。如果读者不了解数据结构的内容,笔者强烈建议补充一下这方面的知识。
5.3.2 队列类Queue
Queue类常用属性和方法如图5.10所示。
图5.10 Queue类的属性和方法
Queue类最主要的方法为入队操作Enqueue和出队操作Dequeue,分别完成在队列尾的添加新元素操作和在队列头的删除元素操作。其他Queue支持的方法,如遍历、清空等,和ArrayList类似,这里不作赘述。
5.3.3 创建队列
【本节示例参考:\示例代码\C05\Queue_Create】
利用Queue的构造函数来创建一个新的队列,常用的形式包括:
public Queue (); public Queue (int capacity); public Queue( int capacity, float growFactor);
参数capacity可以指定所创建列表的初始容量,如果不指定,则初始容量为.NET的默认值32。而参数growFactor则指定当队列满后容量的增长率,新容量等于当前容量与growFactor的乘积,默认值为2.0。下面的代码创建了3个队列对象:
Queue queue1=new Queue(); Queue queue2=new Queue(100); Queue queue3=new Queue(100,1.5f);
其中,queue1的初始容量为32,queue2为100。目前,两者里面都是空的,没有任何元素。随着操作的进行,当列表中的元素达到其最大容量时,列表将自动增长至200。而对于queue3,其初始容量为100,当列表中的元素达到其最大容量时,列表将自动增长至150。
与ArrayList一样,如果想要使用Queue,首先需要在代码头部引入命名空间:
using System.Collections;
5.3.4 元素入队
【本节示例参考:\示例代码\C05\Queue_Enqueue】
可以通过Queue的Enqueue,实现一个元素的入队操作,其形式如下:
public virtual void Enqueue(object obj);
下面的示例中,首先定义了一个队列queue1,然后使用Enqueue方法,向其中添加了3个元素,其中第1个为字符串对象“Hello”,第2个为一个整数对象1,第3个为整数型数组arr1。
代码5-22 使用Enqueue元素入队示例:Default.aspx.cs
1. Queue queue1=new Queue(); 2. 3. //字符串:"Hello"入队 4. object item=new object(); 5. item="Hello"; 6. queue1.Enqueue(item); 7. //整数:1入队 8. item=1; 9. queue1.Enqueue(item); 10. //数组:arr1={1,2,3}入队 11. int[]arr1=new int[]{1,2,3}; 12. queue1.Enqueue(arr1);
队列中的元素如图5.11所示。
图5.11 队列入队示例
5.3.5 元素出队
【本节示例参考:\示例代码\C05\Queue_Dequeue】
与Enqueue相反,可以通过Queue的Dequeue实现一个元素的出队操作,其形式如下:
public virtual object Dequeue();
下面的示例中,首先定义了一个队列queue1,然后使用Enqueue方法,依次入队3个元素。然后再使用Dequeue方法,输出所有的元素。注意,这个输出的顺序和入队的顺序是一致的。
代码5-23 使用Dequeue元素出队示例:Default.aspx.cs
1. Queue queue1=new Queue(); 2. 3. //依次入队:"Hello"、1、{1,2,3} 4. queue1.Enqueue("Hello"); 5. queue1.Enqueue(1); 6. queue1.Enqueue(new int[]{1,2,3}); 7. 8. //通过出队操作,输出队列中的所有元素,这个顺序,与入队的顺序一致 9. object outItem=new object(); 10. while(queue1.Count>0) 11. { 12. outItem=queue1.Dequeue(); 13. Page.Response.Write(outItem+"<br/>"); 14. }
此时,代码输出的结果应与图5.11所示相同。
5.4 堆栈
上面介绍了具有先入先出特征的队列,下面继续介绍另一种具有先入后出特征的集合对象:堆栈。
5.4.1 什么是堆栈
与Queue一样,System.Collections.Stack实际上也是一种操作受限的列表,它要求列表中的元素必须满足先进后出的原则,如图5.12所示。
图5.12 堆栈示意图
5.4.2 堆栈类Stack
Stack类常用属性和方法如图5.13所示。其中,最主要的方法为入栈操作Push和出栈操作Pop,分别完成在堆栈的顶部添加和删除元素的操作。其他Stack支持的方法,如遍历、清空等,和ArrayList类似。
图5.13 Stack类的属性和方法
5.4.3 创建堆栈
【本节示例参考:\示例代码\C05\Stack_Create】
利用Stack的构造函数来创建一个新的堆栈,常用的形式包括:
public Stack (); public Stack (int capacity);
参数capacity可以指定所创建列表的初始容量。如果不指定,则初始容量为.NET的默认值10。当堆栈中的元素达到其最大容量时,其容量将自动增加一倍。下面的代码创建了两个堆栈对象:
Stack Stack1=new Stack(); Stack Stack2=new Stack(100);
其中,Stack1的初始容量为10,Stack2为100。同ArrayList、Queue一样,如果想要使用Stack,首先需要在代码头部引入命名空间:
using System.Collections;
5.4.4 元素入栈
【本节示例参考:\示例代码\C05\Stack_Push】
可以通过Stack的Push,实现一个元素的入栈操作,其形式如下:
public virtual void Push(object obj);
下面的示例中,首先定义了一个堆栈Stack1,然后使用Push方法,向其中添加了3个元素,其中第1个为字符串对象“Hello”,第2个为一个整数对象1,第3个为整数型数组arr1。
代码5-24 使用Push元素入栈示例:Default.aspx.cs
1. Stack stack1=new Stack(); 2. 3. //字符串:"Hello"入栈 4. object item=new object(); 5. item="Hello"; 6. stack1.Push(item); 7. //整数:1入栈 8. item=1; 9. stack1.Push(item); 10. //数组:arr1={1,2,3}入栈 11. int[]arr1=new int[]{1,2,3}; 12. stack1.Push(arr1);
堆栈中的元素是如何存放的?把堆栈假想为一个盛放油饼的框子,那么“Hello”这张饼被压在最下面,整数1在中间,数组{1,2,3}在框子的最上面,效果如图5.14所示。
图5.14 堆栈出栈示例
5.4.5 元素出栈
【本节示例参考:\示例代码\C05\Stack_Pop】
与Push相反,可以通过Stack的Pop实现一个元素的出栈操作,其形式如下:
public virtual object Pop();
在下面的示例中,首先定义了一个堆栈Stack1,然后使用Push方法,依次入队3个元素。再使用Pop方法,输出所有的元素。这个输出的顺序和入队的顺序是相反的。
代码5-25 使用Pop元素出栈示例:Default.aspx.cs
1. Stack stack1=new Stack(); 2. 3. //依次入栈:"Hello"、1、{1,2,3} 4. stack1.Push("Hello"); 5. stack1.Push(1); 6. stack1.Push(new int[]{1,2,3}); 7. 8. //利用Pop方法,循环输出stack1中所有的元素,注意,输出的顺序与入栈的顺序是相反的 9. object outItem=new object(); 10. while(stack1.Count>0) 11. { 12. outItem=stack1.Pop(); 13. Response.Write(outItem+"<br/>"); 14. }
此时,代码输出的结果应与图5.14相同,就像从框子中把油饼逐个取出一样。
承上启下
■ 学完本章后,读者需要回答:
1.什么是数组?.NET基础类库中的System.Array和数组是什么关系?
2.能够使用数组完成以下操作。
(1)元素访问:GetValue
(2)转化类型:ConvertAll
(3)遍历数组:foreach
(4)排序数组:Sort
(5)查找元素:BinarySearch/Contains
(6)反转数组:Reverse
(7)复制数组:Copy/CopyTo
3.C#中的集合命名空间包括哪些类?
4.能够使用ArrayList类完成以下操作。
(1)添加元素:Add/AddRange
(2)插入元素:Insert/InsertRange
(3)删除元素:Remove/RemoveAt/RemoveRange
(4)排序元素:Sort
(5)查找元素:BinarySeach
5.Queue类和Stack类的特点是什么?如何实现队列及堆栈的以下操作。
(1)入队:Enqueue
(2)出队:Dequeue
(3)入栈:Push
(4)出栈:Pop
■ 在下一章中,读者将会了解:
1.利用VS.NET开发环境寻找语法错误和逻辑错误的技术。
2.利用Exception类和try-catch来捕捉程序异常的技术。
3.异常执行的先后顺序。