···························································· 第3 章 chapter3 常用基础类与集合 用C#开发软件的一大优势就是能获得.NETFramework的各种支持,而.NET 的 类库就是其中重要的软件开发资源,它继承了大部分WindowsAPI函数的功能,还提供 了更高级别的操作,如数据访问XML串行化和字符串与集合的处理。离开了这些类库, 就很难编写实用的C#应用程序,即使是简单的控制台程序也要依赖于.NET类库。 对于C#开发人员来说,熟悉常用的类库及其成员是十分重要的,能否熟练地掌握和 使用类库是衡量程序员编程能力的一个很直观的标准。 本章主要内容如下。 (1)常用基础类。 (2)集合和接口。 3.1 常用基础类 3.1.1 .NET Framework 基础类库 .NET的类库提供了各种类、接口、委托、结构和枚举,这些资源按照它们经常的应用 领域分布在不同的命名空间中。我们在1.5.3节中介绍过C#中一些常用的命名空间, 下面再对这些命名空间做进一步详细介绍。 1.System、System.Collections和System.Text System 是.NETFramework的核心类库,包含了运行C#程序必不可少的系统类, 如基本数据类型、基本数学函数、字符串处理、异常处理类等。System.Collections是有 关集合的基本类库,包括实现栈的Stack类和Hashtable类等。System.Text是有关文字 字符的基本类库。 2.System.IO System.IO 是输入输出的基础类库,包含了实现C#程序与操作系统、用户界面及其 他C#程序做数据交换所使用的类,如基本输入输出流、文件输入输出流、二进制输入输 出流、字符读写类流等。 课程练习 6 0 ◆C# 程序设计教程(第2 版·微课版·题库版) 3.System.Windows.Forms和System.Drawing System.Windows.Forms是用来构建Windows窗体的类库,而System.Drawing提 供了基本的图形操作。这两个名字空间为图形用户界面提供了多方面的支持:低级绘图 操作,比如Graphics类等;图形界面组件和布局管理,如Form、Button类等;以及用户界 面交互控制和事件响应,如MouseEventArgs类。利用这些功能,可以很方便地编写出 标准化的应用程序界面。 4.System.Web System.Web是用来实现运行与Internet相关开发的类库,它们组成了ASP.NET网 络应用开发的基础类库。 5.System.Xml和System.Web.Services System.Xml是处理Xml的类库,而System.Web.Services是处理基于Xml的Web 服务的类库。Xml和Web服务是现代程序设计的一种趋势。 6.System.Data System.Data是关于数据及数据库程序设计的。.NETFramework中处理数据库的 技术被称为ADO.NET。 7.System.Net和System.Net.Socket System.Net和System.Net.Socket是关于底层网络通信的。在此基础上,可以开发 具有网络功能的程序,如Telnet、FTP邮件服务等。 8.其他 C#语言中还有其他许多名字空间及类库。如System.Threading是关于多线程 的等。在 高版本的.NETFramework中增加了一些新的基础类库,比如,在.NET3.0/3.5 中,增加了支持LINQ 数据访问技术的System.Linq 和有关工作流开发的System. Workflow等。 由于.NETFramework版本众多,涉及的类库十分庞大,所以本书中将介绍其中最 重要的概念和类库及C#程序设计中最常用的技术。在实际程序设计过程中,要经常参 考.NETFrameworkSDK的文档。如果使用VisualStudioIDE,还可以使用其中的帮助 功能来查阅相关名字空间、类、属性、方法等的说明,有的还有简单的示例。 3.1.2 Math 类 Math类提供了若干实现不同标准数学函数的方法。这些方法都是静态的方法(关 于静态方法请参见4.5.3节),所以在使用时无须创建Math类对象,而直接用类名做前缀 ◆ 第 3 章 常用基础类与集合61 即可调用这些方法。表3-1列出了Math类的常用数学函数。 表3- 1 Math类的常用数学函数 函数名说明 Abs() 返回数的绝对值 Sin(),Cos(),Tan() 标准三角函数 ASin(),ACos(),ATan(),ATan2() 标准反三角函数 Sinh(),Cosh(),Tanh() 标准双曲函数 Max(),Min() 最大值,最小值 Celing() 返回不小于指定数的最小整数 Floor() 返回不大于指定数的最大整数 Round() 返回指定数的四舍五入值 Truncate() 返回数字整数部分 Log(),Log10() 自然对数或以10为底的对数 Exp() 指数函数 Pow() 返回指定数的乘方 Sign() 返回指定数的符号值,负数为1,零为0,正数为1 Sqrt() 返回指定数的平方根 IEEERemainder() 返回两数相除的余数,如Math.IEEERemainder(13.5,3),结果为1.5 【实例3-1】Math类用法实例。源代码如表3-2所示。 表3- 2 实例3-1源代码 行号源代码 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 usingSystem; namespace实例3_1Math类用法{ classProgram{ staticvoidMain(string[]args){ Console.WriteLine("-12的绝对值为:{0}" ,Math.Abs(-12)); Console.WriteLine("不小于-12.567的最小整数为:{0}" , Math.Ceiling(-12.567)); Console.WriteLine("不大于-12.567的最大整数为:{0}" , Math.Floor(-12.567)); Console.WriteLine("-12.567保留为小数的四舍五入值为:{0}" , Math.Round(-12.567,2)); Console.WriteLine("2的指数函数为:{0}" ,Math.Exp(2)); Console.WriteLine("2的次方为:{0}" ,Math.Pow(2,3)); Console.WriteLine("13.5/3余数为:{0}" ,Math.IEEERemainder(13.5,3)); } } } 6 2 ◆C# 程序设计教程(第2 版·微课版·题库版) 3.1.3 DateTime 和TimeSpan 类 1.DateTime类 使用System 命名空间中定义的DateTime类可以完成日期与时间数据的处理工作。 在一个日期时间变量中,可以使用Year、Month、Day、Hour、Minute和Second属性分别 获取年、月、日、时、分、秒的数据信息。 2.TimeSpan类 TimeSpan类表示一个时间间隔。范围在Int64.MinValue~Int64.MaxValue。 【实例3-2】 DateTime类和TimeSpan类用法实例。源代码如表3-3所示。 表3-3 实例3-2源代码 行号源 代 码 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 usingSystem; namespace实例3_2{ classProgram { staticvoidMain(string[]args){ //使用DateTime类创建一个DateTime对象dt,并赋值2022-9-8 DateTimedt=newDateTime(2022,9,8); //将对象dt以短日期格式显示出来 Console.WriteLine(dt.ToShortDateString()); Console.WriteLine("2022年9月8日是本年度的第{0}天",dt.DayOfYear); //输出对象dt的月份值 Console.WriteLine("月份:{0}",dt.Month.ToString()); //使用TimeSpan类创建一个TimeSpan对象ts,并赋值 TimeSpants=dt-DateTime.Now;//DateTime.Now表示当前日期 Console.WriteLine("距离2022年国庆还有{0}天",ts.Days.ToString()); } } } 无论使用DateTime类所创建的对象dt,还是使用TimeSpan类所创建的对象ts都 具有许多属性与方法。例如,下面两个很实用的方法。 (1)IsLeapYear()方法。判断一个年份是否为闰年,如“DateTime.IsLeapYear (2016);”语句返回true。 (2)DaysInMonth()方法。返回指定年份中某个月份的天数,如“DateTime. DaysInMonth(2015,2);”语句返回28。 3.1.4 Random 类 Random 类用来产生随机数。Random 类的Next()方法可产生一个int型随机数; Next(intmaxValue)方法可产生一个小于所指定最大值的非负随机整数;NextDouble() 3.1.3 3.1.4 第◆3 章 常用基础类与集合6 3 方法可产生一个0~1.0的随机数。 例如,下面一段代码能够使用Random 类产生10个[0,100]的随机整数。 Random rd=new Random(); for(int i=0; i<10; i++) { Console.Write("{0},",rd.Next(100)); } 3.1.5 String 类 字符串(String)是引用类型的一种,表示一个Unicode字符序列。一个字符串可存 储约231个Unicode字符。 1.字符串建立 字符串常量是用一对半角双引号("")表示。例如语句: string str="Hello world!"; 在字符串中如果包含了“\”字符,有以下两种处理方法。 第一种方法是采用转义字符。例如: string str="c:\\windows \\OLP.DLL"; 第二种方法是在字符串前面加上字符@,第二种方法中的@字符表示该字符串的所 有字符是其原来的含义,而不解释为转义字符。例如: string str=@"c:\windows\OLP.DLL"; 2.字符串的表示格式 使用string.Format()方法或Console.WriteLine()方法均可以将字符串表示为 规定格式。但这两种方法是完全不同的:string.Format()方法返回一个字符串,而 Console.WriteLine()方法自动调用string.Format()并将格式化后的字符串显示出来。 这两种方法都要用到格式参数,格式参数的一般形式为: {N[,M][:formatcode]} 其中,N 是以0为起始编号的、将被替换的参数号码。M 是一个可选整数,表示最小宽度 值,若M 为负数,则左对齐;若M 为正,则右对齐;若M 大于实际参数的长度,则用空格 填充。formatcode也是一个可选参数,其含义如表3-4所示。 此外,如果标准格式选项不能满足要求,则需要使用形象描述格式。形象描述格式 采用多个形象描述字符来表示输出格式。表3-5给出了一些常用的形象描述字符。 3.1.5 6 4 ◆C# 程序设计教程(第2 版·微课版·题库版) 表3-4 标准格式选项 格式符含 义示例(inti=19;doublex=19.7;) 结 果 C 按金额形式输出Console.WriteLine("{0,8:C}",i); ¥19.00 D 按整数输出Console.WriteLine("{0,8:D}",i); 19 E 科学记数格式Console.WriteLine("{0:E}",i); 1.900000E+001 F 小数点后位数固定Console.WriteLine("{0,8:F3}",x); 19.700 G 使用E和F中合适的一种 N 输出带有千位分隔符的数字Console.WriteLine("{0,8:N3}",19890); 19,890.000 P 百分数格式Console.WriteLine("{0,5:P0}",0.78); 78% 表3-5 常用形象描述字符 形象描述字符含 义示例(doublex=456.78) 结 果 0 数字或0占位符Console.WriteLine("{0:0000.000}",x); 0456.7800 # 数字占位符Console.WriteLine("{0:####.000}",x); 456.7800 . 小数点 , 数字分隔符Console.WriteLine("{0:#,###.000}",3456.78); 3,456.780 % 百分号Console.WriteLine("{0:0.00%}",0.78); 78.00% 【实例3-3】 字符串输出格式。源代码如表3-6所示。 表3-6 实例3-3源代码 行号源 代 码 01 02 03 04 05 06 07 08 09 10 11 12 13 14 usingSystem; namespace实例3_3字符串输出格式{ classProgram { staticvoidMain(string[]args){ doublex=3456.78; strings0=string.Format("{0,10:F3}",x); strings1=string.Format("{0:#######.0000}",x); Console.WriteLine(s0); Console.WriteLine(s1); Console.WriteLine("{0,10:f3},{1,10:E}",x,x); Console.ReadLine(); } } } 输出结果为: 3456.780 3456.7800 第◆3 章 常用基础类与集合6 5 3456.780,3.456780E+003 请按任意键继续… 3.常用的字符串操作方法 表3-7列举了常用的字符串操作方法。有的方法有多种重载(关于重载的概念参见 4.5.4节)。 表3-7 常用的字符串操作方法 方法名称方法格式功能说明 比较两个字符串 string.Compare(stringstrA,string strB) 如果strA大于strB,结果为1; 如果strA小于strB,结果为-1; 如果strA等于strB,结果为0 string.Compare(stringstrA,string strB,boolignoreCase) 比较两个字符串时是否忽略大小写,true表 示忽略大小写,false表示区分大小写 string.Equals(string strA,string strB) 两串相等返回true,否则返回false 字符串是否为空string.IsNullOrEmpty(stringstr) 判断str是否为空,返回bool值 查找 strS.IndexOf(charvalue) 返回字符value在字符串strS中首次出现的 位置。注意,起始位置从0开始。返回结果 为整数 stS.IndexOf(stringvalue) 返回字符串value在字符串strS中首次出现 的位置。返回结果为整数 strS.IndexOf(charvalue,int startIndex) 在字符串strS中从第startIndex个字符开始 查找字符value首次出现的位置 strS.LastIndexOf(stringstr) 返回字符串str在strS 中最后一次出现的 位置 插入strS.Insert(int startIndex,string str) 在字符串strS的第startIndex位置插入字符 串str 删除strS.Remove(intstartIndex,int count) 在strS中删除从startIndex开始的count个 字符串 替换strS.Replace (stringoldStr,string newStr) 将字符串strS中所有oldStr替换为newStr 分离stS.Split(char[]separator) 将字符串strS按照指定的字符进行分割,返 回string型数组 strS.ToCharArray() 将字符串strS分割成字符,返回char型数组 取子串strS.Substring (intstartIndex,int length) 从字符串strS的startIndex开始取length个 字符 6 6 ◆C# 程序设计教程(第2 版·微课版·题库版) 续表 方法名称方法格式功能说明 大小写转换strS.ToUpper() 将字符串strS全部转换为大写 strS.ToLower() 将字符串strS全部转换为小写 去掉空格 strS.TrimStart() 删除字符串strS左端的空格 strS.TrimEnd() 删除字符串strS右端的空格 strS.Trim () 删除字符串strS左右两端的空格 字符串是否含有 数字Char.IsNumber(stringstrS,int index) 判断字符串strS第index位置的字符是否是 数字,是,返回true值;否,返回false 说明:在表3-7中,通过类名string调用的方法是静态方法,通过字符串实例strS调 用的方法是实例方法,在实际应用时要正确调用。 【实例3-4】 假设有一字符串strS="ThisIsAnApple.",使用字符串方法,完成下 面的要求。源代码如表3-8所示。 (1)取出字符串strS中的第9个字符,然后统计该字符在字符串strS中出现的 次数。 (2)统计字符串中单词的个数。 (3)将字符串反序并全部转换为大写字符输出。 表3-8 实例3-4源代码 行号源 代 码 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 usingSystem; namespace实例3_4{ classProgram { staticvoidMain(string[]args){ stringstrS="ThisIsAnApple."; if(!string.IsNullOrEmpty(strS) { charfindChar=Convert.ToChar(strS.Substring(8,1));//取出字符串中的第9个字符 intcount=GetCharCount(strS,findChar); //统计指定的字符出现的次数 string[]word=strS.Split(' '); //将字符串strS按照空格分割成数组 intlen=word.Length; //统计单词的个数 stringstrSReverse=MyReverse(strS).ToUpper(); //将字符串反序,并转换为大写 Console.WriteLine("\"ThisIsAnApple.\"共有{0}个单词,{1}出现了{2}次",len, findChar,count); Console.WriteLine(strSReverse); } else Console.WriteLine("字符串为空"); Console.ReadKey(); } 第◆3 章 常用基础类与集合6 7 续表 行号源 代 码 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 publicstaticintGetCharCount(stringstrS,charfindChar){ intcount=0; char[]c=strS.ToCharArray(); //将字符串分割成字符数组 for(inti=c.Length-1;i>=0;i--){ if(c[i]==findChar) count++; } returncount; } publicstaticstringMyReverse(stringstrS){ stringstrReverse=""; char[]c=strS.ToCharArray(); //将字符串分割成字符数组 for(inti=c.Length-1;i>=0;i--) { strReverse+=c[i].ToString(); } returnstrReverse; } } } 3.1.6 StringBuilder 类 前面介绍过,String类的索引函数是只读的,其各种操作方法不是修改字符串本身, 而是生成新的字符串。由于String的值一旦建立就不能修改,修改String的值实际上是 返回一个包含新内容的新的String实例。显然,如果这种操作非常多,对内存的消耗是 很大的。 例如,下面的代码并不能改变字符串str的内容。 string str="C#"; str+="实例教程"; str=str.Substring(4, 2); 准确地说,一旦创建了一个String对象,其内容就是不可变的,每次操作都是生成一 个新字符串,而后将当前对象的引用指向新字符串,如图3-1所示。 图3-1 字符串对象操作示意图 6 8 ◆C# 程序设计教程(第2 版·微课版·题库版) 此过程中一共生成了4个字符串对象(包括常量对象"实例教程"),前3个字符串将 脱离程序的控制范围(没有任何对象指向它们),等待CLR 进行回收。对于很长的或是 需要频繁操作的字符串,这样往往会消耗大量的系统资源。 .NET类库专门提供了一个StringBuilder类(位于System.Text命名空间下),它对 字符串进行动态管理,即允许直接修改字符串本身的内容,而不是每次操作都生成新 字符串。使用StringBuilder类每次重新生成新字符串时不再生成一个新实例,而是在 原来字符串占用的内存空间上处理,而且它可以动态地分配占用的内存空间大小。因 此,在字符串处理操作比较多的情况下,使用StringBuilder类可以显著提高系统性能。 StringBuilder与String类的用法有很多相似之处,包括通过Length属性来获取长 度,通过索引函数(在StringBuilder中是可读写的)来访问字符,以及Insert、Remove、 Replace这些子串操作方法。尽管这些方法的返回类型也为StringBuilder,但并没有创 建新的对象,返回值也就是调用这些方法的对象本身。通过StringBuilder类的ToString 方法就可以获得其中的字符串。 StringBuilder类还提供了Capacity和MaxCapacity属性,分别表示字符串的初始容 量和最大容量。应尽量为StringBuilder对象指定合适的初始容量,如果过大就会占用不 必要的内存空间,过小则会导致频繁的重新调整。如果某些操作使字符串超出了初始空 间,那么StringBuilder对象会自动增加内存空间。该类特有的3 个方法是Append、 AppendLine和AppendFormat,它们都用于在字符串的尾端追加新内容。表3-9列出了 StringBuilder类特有的属性和方法。 表3-9 StringBuilder类特有的属性和方法 属 性描 述 Capacity StringBuilder实例的初始容量,可读可写 MaxCapacity StringBuilder实例的最大容量,只读 方 法描 述 Append 向StringBuilder实例的尾端追加字符串 AppendLine 向StringBuilder实例的尾端追加一行字符串 AppendFormat 按照指定的格式向StringBuilder实例的尾端追加字符串 例如在实例3-4中,字符串反序的方法MyReverse总共创建了2倍的c.Length个字 符串实例。使用StringBuilder只在一个实例上进行操作,方法代码修改如下所示。 public static string MyReverse(string strS) { StringBuilder strReverse=new StringBuilder(); char[]c=strS.ToCharArray(); //将字符串分割成字符数组 for(int i=c.Length-1; i>=0; i--) { strReverse.Append(c[i]); 第◆3 章 常用基础类与集合6 9 } return strReverse.ToString(); } 3.1.7 Array 类 在.NETFramework环境中,人们并不能直接创建Array类型的变量,但是所有的 数组都可以隐式地转换为Array类型。这样一来,就可以在数组中使用Array类中定义 的一系列属性和方法了。下面重点介绍这些属性、方法中经常用到的几个。 1.Rank属性、Length属性 Rank属性用于获取数组的维数(又称为秩)。Length属性用于获取数组所有维数中 元素的总和。Rank属性和Length属性只能用于数组对象。 2.GetLength()、GetLowerBound()、GetUpperBound()方法 GetLength(dimension)方法用于获取指定维中元素的个数。GetLowerBound (dimension)和GetUpperBound (dimension)方法分别用于获取指定维的下界和上界。 这3个方法也只能用于数组对象。 3.Sort()方法 Sort(array)方法用于对指定数组升序排序。 int[]nums={2, 7, 5, 3, 6}; Array.Sort(nums); 执行上面语句之后,数组nums中各数组元素已经按升序顺序进行了排列,各数组元 素依次为2,3,5,6,7。 4.Reverse()方法 Reverse(array)用于对数组元素进行逆序,即首尾倒置。 int[]nums={2, 7, 5, 3, 6}; Array.Reverse(nums); 执行上面语句之后,数组nums中各数组元素依次为6,3,5,7,2。 5.Copy()方法 Array.Copy(nums,destArray,destArray.Length)用于将源数组中的元素复制到目 标数组中。该方法中可以省略第三个参数,表示所有元素。 int[]nums={2, 7, 5, 3, 6}; int []arrb=new int [nums.Length]; 7 0 ◆C# 程序设计教程(第2 版·微课版·题库版) Array.Copy(nums,arrb) 上面语句实现了将数组nums中各元素复制到数组arrb中。 6.IndexOf()方法 IndexOf(array,value,startindex)方法返回指定数组中、从指定位置开始、与value 匹配的元素的位置,返回值类型为整型。该方法中可以省略第三个参数,表示从头开始 查找。 int[]nums={2, 7, 5, 3, 6}; int n=Array.IndexOf(nums,5,0); 执行上面的语句之后,n的值为2。 7.BinarySearch方法 BinarySearch(array,value)方法对已排序的数组使用二分查找法进行搜索。返回 值类型为整型,表示值为value的元素在已排序数组中的位置。如果未找到返回一个 负值。 int[]nums={2, 7, 5, 3, 6}; Array.Sort(nums); //排序,排序后nums 中各元素为2,3,5,6,7 int n=Array.BinarySearch(nums, 6); //二分查找 执行上面的语句,n的值为3,表示值为6的元素在第4个位置。 8.Clear()方法 Clear(array,index,length)方法将数组中的一系列元素置零、false或null,具体取决 于元素类型。 int[]nums={2, 7, 5, 3, 6}; Array.Clear(nums,2,3); 执行上面的语句,数组nums中数组元素依次为:2,7,0,0,0。 3.1.8 并行计算 在.NET4.0之前开发并行程序非常困难。在.NET4.0中,C#通过引入Parallel 类,提供了对并行开发的支持。Parallel类提供了Parallel.For、Parallel.ForEach、 Parallel.Invoke等方法,其中Parallel.Invoke用于并行调用多个任务。下面通过Parallel.For 应用实例展示Parallel类的基本用法。 【实例3-5】 使用Parallel类进行并行计算。创建该实例的步骤如下。启动 VS2022,依次选择“文件”→“新建”→“项目”→“控制台应用”命令,打开Program.cs文 件,用下面代码替换原先代码,按F5键运行。实例源代码如表3-10所示。 ◆ 第 3 章 常用基础类与集合71 表3-10 实例3-5源代码 行号源代码 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 usingSystem; usingSystem.Threading; usingSystem.Threading.Tasks; namespaceytu{ classmyParallel{ staticvoidMain(string[]args){ Normal(); ParallelFor(); Console.Read(); } publicstaticvoidNormal(){ DateTimedt=DateTime.Now; for(inti=0;i<10;i++){ for(varj=0;j<10;j++)DoSomething(); } Console.WriteLine("Normal方法耗时{0}" , (DateTime.Now-dt).TotalMilliseconds.ToString()); } publicstaticvoidParallelFor(){ DateTimedt=DateTime.Now; Parallel.For(0,10,i=> { for(intj=0;j<10;j++)DoSomething(); }); Console.WriteLine("ParallelFor方法耗时{0}" , (DateTime.Now-dt).TotalMilliseconds.ToString()); } publicstaticvoidDoSomething(){ Thread.Sleep(100); } } } 运行结果如图3-2所示。 图3- 2 实例3-5并行计算运行结果 7 2 ◆C# 程序设计教程(第2 版·微课版·题库版) 3.2 集  合 如果将紧密相关的数据组合到一个集合中,则能够更有效地处理这些紧密相关的数 据。这是由于集合类(Collections)具有自动内存管理功能、支持枚举访问,某些集合类还 具有排序和索引功能等。合理地使用集合可以简化代码数量,提高代码效率。 3.2.1 什么是集合 集合是一组组合在一起的类似的类型化对象。在.NET中提供了专门用于存储大量 元素的集合类(Collections)。这些集合通常可以分为表3-11所示的3种类型。 表3-11 Collections类的类型 类 型描 述 常用集合 这些集合是数据集合的常见变体,如动态数组(ArrayList)、哈希表(Hashtable)、队列 (Queue)、堆栈(Stack)、SortedList等。常用集合有泛型和非泛型之分 位集合 这些集合中的元素均为位标志,它们的行为与其他集合稍有不同 专用集合这些集合具有专门的用途,通常用于处理特定的元素类型,如StringDictionary C#2.0引入了泛型的概念之后(关于泛型的概念将在4.11.1节中介绍),表3-11中 的常用集合就有了泛型集合与非泛型集合之分。.NETFramework2.0版类库提供一个 名为System.Collections.Generic的命名空间,其中包含了基于泛型的集合类。非泛型集 合与泛型集合主要区别如下。 (1)所有非泛型集合类都有一个共同的特征(除BitArray以外,它存储布尔值),那 就是弱类型。换句话说,它们存储System.Object的实例。弱类型使集合能够存储任何 类型的数据,因为所有数据类型都是直接或间接地从System.Object派生得来。但是,弱 类型也意味着使用者需要对集合中的元素执行附加的处理,例如装箱、拆箱或转换,这些 操作会影响集合的性能。 (2)与所有非泛型集合不同,泛型集合同时具备可重用性、类型安全和效率,这是非 泛型集合无法具备的。 由于泛型集合与非泛型集合存在这种区别,C# 2.0之后的应用程序在使用集合类 时几乎都采用泛型集合类。掌握泛型集合类的使用是大家学习重点。因此,本节重点介 绍动态数组(ArrayList)、哈希表(Hashtable)、队列(Queue)、堆栈(Stack)、SortedList 5个常用的非泛型集合,以及Icollection、Ienumerable和Ilis3个常用的接口。 3.2.2 ArrayList 我们知道,数组在用new创建后,其大小(Length)是不能改变的,而ArrayList中的 数组元素的个数(Count)是可以改变的,元素可以随意添加、插入或移除,ArrayList实际 第◆3 章 常用基础类与集合7 3 上是C#中的动态数组。 在ArrayList类型的数据中,成员都为object类型,这样就可以存放任意类型的数据 了。定义ArrayList类型变量时,可以使用如下格式: ArrayList <数组名称>=new Arraylist(); 在定义ArrayList类型变量后,就可以使用一些方法和属性来操作数组。表3-12列 出了ArrayList常用的属性与方法。 表3-12 ArrayList属性与方法 属 性描 述属 性描 述 Capacity ArrayList 的容量,容量是指 ArrayList中可包含的元素数Count ArrayList的元素个数,指ArrayList 中实际包含的元素数 方 法描 述方 法描 述 Add 向数组增加一个元素AddRange 向数组增加一定范围内的元素 Clear 清除所有元素Contains 判断某个元素是否在数组中 Insert 使用索引插入某个元素Remove 删除某个元素 RemoveAt 使用索引来指定要删除条目的位置ToArray 将ArrayList元素复制到指定数组中 IndexOf 查找某个元素的索引 下面通过一个实例说明ArrayList类的使用。 【实例3-6】 ArrayList类用法实例。源代码如表3-13所示。 表3-13 实例3-6源代码 行号源 代 码 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 usingSystem; usingSystem.Collections; namespace实例3_6{ classProgram{ staticvoidMain(string[]args){ ArrayListmyAL=newArrayList(); myAL.Capacity=6; //当容量超过6时,其容量自动增加一倍 for(inti=0;i<10;i++){ myAL.Add(i); } myAL.RemoveAt(2); //删除索引为2的数据,即第三个数据 myAL.Reverse(); foreach(intiteminmyAL) Console.Write("{0},",item.ToString()); Console.WriteLine("元素个数:{0}",myAL.Count); Console.WriteLine("动态数组|容量:{0}",myAL.Capacity); Console.ReadKey(); } } } 7 4 ◆C# 程序设计教程(第2 版·微课版·题库版) 针对上面实例运行结果,对ArrayList类解释如下。 (1)注意区别ArrayList的容量(Capacity)和元素个数(Count)两个属性。容量是指 ArrayList能装多少个元素,元素个数是指此时到底装了多少个元素。 (2)对于ArrayList类,当元素个数超过容量时,其容量会自动增加一倍。例如,在 图3-3 实例3-6运行结果 上面的例子中,设置ArrayList的容量为2,然后往 里面添加3个元素。此时元素个数超出其容量,所 以容量自动增长为4,容量仍然不足,再次自动增长 为8。 运行结果如图3-3所示。 ArrayList尽管扩充了数组的功能,但是同数 组相比,ArrayList也有缺点:ArrayList只能是一 维的,而数组可以是多维的;ArrayList下标必须从零开始,而数组下标可以不从零开始; 另外数组执行效率也高于ArrayList。 3.2.3 Hashtable 1.Hashtable概述 Hashtable通常被称为哈希表。在Hashtable类型的变量中的每一个元素都以“键 (Key)-值(Value)”对的格式保存。Hashtable中键(Key)唯一,不能有重复值,不能为空值, 但值(Value)可以为空。简单地说,Hashtable像一个字典,根据键可以查找到相应的值。 Hashtable中的“键-值”对均为object类型,所以Hashtable可以支持任何类型的 “键-值”对。当然,这一特性也将影响Hashtable的性能。为此,在C# 2.0之后,建议采 用Hashtable的泛型版本:Dictionary。 Hashtable常用的属性与方法如表3-14所示。 表3-14 Hashtable常用的属性与方法 属 性描 述属 性描 述 Count 表示哈希表中元素的个数Keys 表示哈希表中所有键的集合 Values 表示哈希表中所有列的集合 方 法描 述方 法描 述 Add 向哈希表末尾增加一个元素Clear 清除哈希表中所有元素 Contains 判断哈希表中是否包含该键ContainsValue 判断哈希表中是否包含该值 Remove 删除哈希表中一个元素 以下代码演示了Hashtable属性和方法的最基本用法: Hashtable hst=new Hashtable(); //声明Hashtable 对象 hst.Add("郭晓冬", 235); //添加元素(键和值) if (!hst.Contains ("赵刚")) { 3.2.3 第◆3 章 常用基础类与集合7 5 hst.Add("赵刚", 143); }h st["郭晓冬"]=200; //修改元素 hst["赵刚"]=(int)hst["赵刚"]+10; hst.Remove("赵刚"); //移除元素 foreach (DictionaryEntry item in hst) // 输 出 元素 Console.WriteLine("{0},{1}", item.Key, item.Value); C#中提供了foreach语句以对Hashtable进行遍历。由于Hashtable的元素是一个 “键-值”对,因此需要使用DictionaryEntry类型来进行遍历。DictionaryEntry类型在此 处表示一个“键-值”对的集合。 2.Hashtable实例 Hashtable是以一种“键-值”对的形式存在的,因此要通过键来访问Hashtable中的 值,即Hashtable[key]。以下代码演示了Hashtable最基本的用法。 【实例3-7】 车辆进出闸口自动刷卡扣费,Hashtable用于检查是否重复读卡。 问题的提出:由于进出闸口读卡器距离很近,存在刷一次卡,被入口读卡器、出口读 卡器同时读到的问题。解决办法是:由于哈希表的key为卡的ID+IP、value为时间,这 样,只要保证目标读卡器首先读到信号,其他读卡器再读到信号也会被当作重复读卡 处理。部 分源代码如表3-15所示。 表3-15 实例3-7部分源代码 行号部分源代码 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 classProgram{ staticHashtablehsTable1=new Hashtable(); publicstaticboolRepeat(stringkey,intinterval){ try{ if(!hsTable1.Contains(key)) //如果key未保存在哈希表中 { hsTable1.Add(key,DateTime.Now); returnfalse; } //如果key已存在哈希表中,则两种情况:超过时限间隔的(可能是返回车辆)、 //重复读卡 //其中,重复读卡又可能是:同一读卡器、不同读卡器 TimeSpants=DateTime.Now-Convert.ToDateTime(hsTable1[key]); if(ts.TotalSeconds>interval) { hsTable1[key]=DateTime.Now; returnfalse; } 7 6 ◆C# 程序设计教程(第2 版·微课版·题库版) 续表 行号部分源代码 19 20 21 22 23 24 25 26 27 28 29 //属于重复读卡 returntrue; } catch(Exceptionex) { throwex; } } staticvoidMain(string[]args) { Console.WriteLine(Repeat("KEY1",3)); //interval设置为3s System.Threading.Thread.Sleep(4000); //使用线程延迟4s Console.WriteLine(Repeat("KEY1",3)); } } 3.Hashtable的优点 Hashtable的基本原理是通过节点的关键码确定节点的存储位置,即给定节点的关 键码k,通过一定的函数关系H (散列函数),得到函数值H (k),将此值解释为该节点的 存储地址。因此,Hashtable的优点主要在于其索引的方式:不是通过简单的索引号,而 是采用一个键(key)。这样可以方便地查找Hashtable中的元素,而且查找速度非常快, 在对速度要求比较高的场合可以考虑使用Hashtable。 3.2.4 Queue 和Stack 1.Queue和Stack的概念 Queue(队列)和Stack(栈)是两种重要的线性数据结构。队列遵循“先进先出”(First InFirstOut,FIFO)的原则,而栈则遵循“后进先出”(LastInFirstOut,LIFO)的原则。 队列的特性就是固定在一端输入数据(称为入队,Enqueue),另一端输出数据(称为 出队,Dequeue)。队列中数据的插入必须在队头进行,删除数据必须在队尾进行,而不能 直接在任何位置插入和删除数据。 栈只能在一端输入输出,它有一个固定的栈底和一个浮动的栈顶。栈顶可以理解为 是一个永远指向栈最上面元素的指针。向栈中输入数据的操作称为“压栈”,被压入的数 据保存在栈顶,并同时使栈顶指针上浮一格。从栈中输出数据的操作称为“弹栈”,被弹 出的总是栈顶指针指向的位于栈顶的元素。如果栈顶指针指向了栈底,则说明当前的栈 是空的。 当需要临时存储信息时(也就是说,可能想在检索了元素的值后放弃该元素),栈和 队列都很有用。如果需要按照信息存储在集合中的顺序来访问这些信息,则应使用队 列。如果需要以相反的顺序访问这些信息,则应使用栈。 3.2.4 ◆ 第 3 章 常用基础类与集合77 2.Queue操作 对Queue及其元素执行的操作主要有如下3种。 (1)Enqueue():将一个元素添加到Queue的末尾。 (2)Dequeue():从Queue的开始处移除最旧的元素。 (3)Pek():从Queue的开始处返回最旧的元素,但不将其从Queue中移除。 【实例3-8】Queue的操作。源代码如表3-16所示。输出结果如图3-4所示。 表3-16 实例3-8源代码 行号源代码 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 usingSystem; usingSystem.Collections; namespace3_8{ classProgram { staticvoidMain(string[]args){ string[]months={"January" ,"February" ,"March" ,"April" ,"May"}; Queuequeue=newQueue(); //使用Enqueue()方法将一个元素添加到Queue的末尾 foreach(stringiteminmonths) queue.Enqueue(item); Console.WriteLine("队列中元素个数是:{0}" ,queue.Count); //使用Dequeue()从Queue的开始处移除最旧的元素 while(queue.Count>0) Console.WriteLine(queue.Dequeue()); } } } 3.Stack操作 对Stack及其元素执行的操作主要有如下3种。 (1)Push():将指定对象压入栈中。 (2)Pop():将最上面的元素从栈中取出,并返回这个对象。 (3)Pek():返回栈顶元素,但不将此对象弹出。 【实例3-9】Stack的操作。源代码如表3-17所示。运行结果如图3-5所示。 表3-17 实例3-9源代码 01 usigSystem; 02 usnsem.ocin igSy(n) tCletos; 03 namespaceStack_Sample{ 04 clasProgram { 05 staticvoidMain(string[]args){ 行号源代码 7 8 ◆C# 程序设计教程(第2 版·微课版·题库版) 续表 行号源 代 码 06 07 08 09 10 11 12 13 14 string[]months={"January","February","March","April","May"}; Stackstack=newStack(); foreach(stringiteminmonths) stack.Push(item); //入栈 while(stack.Count>0) Console.WriteLine(stack.Pop()); //出栈 } } } 图3-4 实例3-8运行结果 图3-5 实例3-9运行结果 通过上面的实例可以看出: (1)Stack与Queue在用法上非常类似,但从二者的运行结果不难看出,Stack与 Queue对数据处理方式是截然不同的。 (2)连续输出Queue 和Stack 中的数据时,使用while(queue.Count>0)和 while(stack.Count>0),不能使用for循环,因为Count属性的值在不断减少。 3.2.5 SortedList 类 SortedList提供了类似于ArrayList和Hashtable的特性,可以将其理解为一种结合 体。SortedList的元素是“键-值”对,这点与Hashtable相似;而其提供了索引的方法,这 点又与ArrayList类似。 1.SortedList的概念 SortedList表示“键-值”对的集合。使用两个数组存储其数据,一个保存关键字,另 一个用于存放值。因此,列表中的一个表项由“关键字-值”对组成。SortedList不允许出 现重复的关键字,并且关键字也不允许是null引用。 SortedList以关键字顺序维持数据项,并且能够根据关键字或者索引来检索它们。 SortedList的常用属性和方法与前面介绍的Hashtable(参见3.2.3节)的属性和方法完全 一样,在此不再赘述。