版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
第4章高级C#概念4.1集合
4.2异常
4.3属性
4.4索引器
4.5委托
4.6事件
4.7综合案例:处理SARS紧急事件本章小结
练习与作业
上机部分(四)
学习目标●使用集合●理解异常处理机制●使用属性●使用索引器●理解委托和事件机制及其使用4.1集合
.NET框架的基类库提供了一组“集合”,这是一组通用的数据类型。这些数据类型为对数据进行集中的存储和操作提供了可能。在System.Collections命名空间中包含了许多接口和类,它们定义了对象的各种集合,如列表、队列、数组和字典等。表4-1显示了System.Collections命名空间中的一些集合类。要想学习这些类的使用,可以逐个去研究这些类的方法和属性,但是更好的学习方法是先了解这些集合类可实现的基本接口。我们知道,System.Collection命名空间下也定义了一些接口,每个接口都代表了集合的某个特定功能或特性。表4-2显示了System.Collection命名空间定义的接口。要想成为一个“合格”的集合类,必须去全部或部分实现这些接口,以满足集合的特性。下面我们就先从几个典型的接口入手,看看它们是如何对集合类进行约束的。表4-1System.Collections命名空间中的集合类表4-2System.Collections下的接口4.1.1ICollection、IList和IDictionary接口
1.ICollection和枚举器
ICollection接口定义了类似集合的对象所应具备的最基本的特性,所有的集合类都能够实现这个接口。ICollection接口公开了一个只读的Count属性和CopyTo方法,该方法能够把集合对象中的元素复制给数组。此外,ICollection接口还实现了IEnumerable接口,IEnumerable接口定义了GetEnumerator()方法,该方法返回一个枚举器对象。枚举器可以用来读取集合中的数据,但是不能修改这些数据。我们常用的foreach循环支持枚举器。这样一来,所有的集合类都可以通过实现这个接口获得一个枚举器对象,当然我们也可以使用foreach循环达到快速遍历的效果。
注意:枚举器和枚举两个术语很容易混淆,但它们是完全不同的概念。枚举是一种命名常量的特殊类型,如:enumColor{Red,Green,Blue}
我们可以使用IEnumerator接口操纵枚举器对象。IEnumerator接口中的属性和方法见表4-3。最初,枚举器被定位于集合的第一个元素之前。Reset也是将枚举器返回到此位置。在创建枚举器或调用Reset方法后,如果要读取Current的值,就应该调用MoveNext方法将枚举器移动到集合的第一个元素。当越过集合的末尾之后,枚举器将返回无效状态。这时调用MoveNext方法将会返回false。此时调用Current将会得到异常。表4-3
IEnumerator接口中的属性和方法【例4-1】下面的代码显示了如何遍历一个数组。关于集合的遍历与此类似。
staticvoidMain(){int[]arr=newint[]{1,2,3};IEnumeratore=arr.GetEnumerator();while(e.MoveNext())Console.WriteLine("number:{0}",e.Current);}
程序运行结果如图4-1所示。图4-1例4-1运行结果
如果一个集合类支持枚举器访问,那么我们也可以使用foreach的方法达到快速遍历的效果。上面的代码也可以写成:
staticvoidMain(){int[]arr=newint[]{1,2,3};foreach(intxinarr)Console.WriteLine("number:{0}",x);}2.IList和IDictionary接口
IList接口表示可单独索引的有序对象集合。Array、ArrayList、StringCollection等类实现了这个接口。IList接口继承自ICollection接口,除此之外,它还定义了自己的属性和方法。表4-4总结了IList接口中的属性和方法。表4-4List接口中的属性和方法IDictionary是一个关联键/值对的集合的接口。每个关联必须具有一个唯一的非空引用键,但关联的值可以是任何类型的对象,包括null。SortedList、Hashtable和DictionaryBase是实现了该接口的集合类。
IDictionary接口的实现分为以下几种类型:
(1)只读,不能修改只读的IDictionary。
(2)固定大小,固定大小的IDictionary不允许添加和删除元素,但可以修改现有元素。
(3)可变大小,可变大小的IDictionary允许添加、修改和删除元素。表4-5总结了IDictionary接口中的成员。表4-5IDictionary接口中的成员4.1.2ArrayList类
ArrayList是实现了IList接口的集合类,我们可以将其视作Array和Collection对象的混合体。对于该类中的元素,既可以像处理数组一样通过元素索引确定其地址,对元素进行排序和搜索,也可以象处理集合那样添加一个元素,在给定位置插入元素或删除元素。
ArrayList和Array之间的区别如下:
Array是抽象类,所有的数组均派生自此类;ArrayList是一个普通类,可以被实例化。
在Array中一次只能获取或设置一个元素的值;ArrayList则提供了操作元素的一系列方法,包括插入、删除和添加等。
ArrayList具有Capacity属性。使用此属性可以使ArrayList的容量自动扩展。如果属性ArrayList.Capacity的值被修改,则会自动进行内存的重新分配和元素的复制。这样,当有元素被添加到ArrayList中时,其容量的上限会动态增加。而Array的容量是固定的,因而不会被修改。
可以设置Array的下限,而ArrayList的下限始终为0。
Array可以有多维,而ArrayList只有一维。
【例4-2】
下面的代码显示了如何使用ArrayList类,添加元素和遍历。
usingSystem;usingSystem.Collections;classClass2{ staticvoidMain(string[]args) { ArrayListmyal=newArrayList(); //创建ArrayList对象
myal.Add("欢迎"); //添加元素
myal.Add("使用"); myal.Add("C#"); myal.Add("!"); Console.WriteLine("\n\t我的表单"); Console.WriteLine("数量:{0}",myal.Count); Console.WriteLine("容量:{0}",myal.Capacity); Console.Write("内容:"); IEnumeratormyErtor=myal.GetEnumerator(); //获得枚举器while(myErtor.MoveNext()) //遍历元素
Console.Write("{0}",myErtor.Current); Console.WriteLine(); }}
上述示例的运行结果如图4-2所示。注意,ArrayList的数量和容量往往是不一致的。ArrayList的初始容量是16,当数量超过ArrayList的容量时,其容量Capacity会增长一倍。此外,该示例中还使用了IEnumerator接口来访问ArrayList中的每一个元素,这是因为ArrayList集合实现了IEnumerable接口,并实现了它的唯一方法GetEnumerator。
GetEnumerator方法返回一个实现了IEnumerator接口的遍历器,该遍历器用于访问ArrayList集合中的每一个元素。图4-2例4-2运行结果4.1.3SortedList类
SortedList类实现了IDictionary接口,该类在内部维护了两个动态数组,一个用于存放键,另一个用于存放相关联的值,SortedList对象中的每一个元素都是一个键/值对。注意,无论何时,SortedList都不允许有重复的键。
Count属性用来获取SortedList所包含的元素个数,Add方法用来向SortedList添加一个元素。
【例4-3】下面的代码显示了如何使用SortedList,添加元素和遍历。
usingSystem;usingSystem.Collections;classClass1{ staticvoidMain(string[]args) { SortedListmysl=newSortedList(); //创建SortedList对象
mysl.Add("First",1); //添加元素
mysl.Add("Third",3); mysl.Add("Second",2); Console.WriteLine("Count:{0}",mysl.Count); Console.WriteLine("\t-KEY-\t-VALUE-"); for(inti=0;i<mysl.Count;i++) //遍历,打印键和值
{Console.WriteLine("\t{0}:\t{1}",mysl.GetKey(i),mysl.GetByIndex(i)); } }}
上述示例的运行结果如图4-3所示。我们可以发现,SortedList会即时保持元素的排序状态,所以在SortedList上的操作要比其他的集合类慢。这是一种典型的牺牲存储速度而提高访问速度的例子。注意:只有当编程确实需要较高的灵活性时,才应当使用SortedList对象。图4-3例4-3运行结果4.2异常异常是程序运行时产生的错误,这种错误和编译错误不同,一般很难在编译时被发现,而是在程序运行时方产生。下面的代码在编译时没有问题,但当我们点击【运行】按钮时将在第2行弹出图4-4所示的异常窗口。
VS.NET2005帮助我们在开发程序时很清楚地定位异常产生的原始代码。然而异常的产生往往是不可预见的,若在开发环节没有捕获异常,则发布以后的程序仍然不是一个好的程序,当最终用户不小心触发了潜藏的异常时,程序将会发生崩溃,如图4-5所示。图4-4异常的产生图4-5发生崩溃时的对话框
那么,如何捕获这些异常错误,使程序能最大限度的安全呢?C#采用结构化的异常处理语句,使我们能够以标准化和可控制的方式处理运行时错误。而在传统的解决方案中,方法失败时必须返回错误代码,而且每次调用方法时都必须手动检验这些值。此过程不但冗长,而且容易出错。比如,针对上面的代码,如果我们作出这样的改动,显然不是最佳的解决方法。inti1=0;boolflag=true;if(i1==0){flag=false;return;}intres=5/i1;Console.WriteLine(res.ToString());
因此,要想编写健壮的程序,上述简单的错误处理技术是不能胜任的。这就体现了当错误产生时由运行库自己生成异常的重要性。这些异常提供了关于错误的详细信息。4.2.1System.Exception
在C#中,异常用类来表示。.NET框架提供了大量的异常类,这些类存储了各种异常的相关信息和帮助。异常类的层次结构如图4-6所示。图4-6异常类的层次结构
如图4-6所示,所有的异常类都必须从内部异常类Exception派生而来,而Exception是System命名空间的一部分。因此,所有的异常类都是Exception类的子类。例如,当试图除以0时,系统将产生DividedByZeroException异常,该异常是算术异常ArithmeticException的直接派生类。
C#的异常处理机制规定,一旦出现运行时错误,系统将会产生一个包含该错误信息的异常对象。该对象一旦产生,就会由相应的异常处理程序捕获并进行处理。表4-6描述了一些Exception基类的属性,它们可以帮助你理解代码位置、类型和异常的发生原因。任何继承自该基类的异常都将提供这些属性。表4-6System.Exception的属性Exception对象在 .NET框架中定义,完整的名字是System.Exception。但是一般情况下,.NET的类和应用程序不抛出这种原始异常。框架定义了另两种功能相同的类:System.SystemException和System.ApplicationException。几乎所有 .NET框架中定义的异常对象都继承自SystemException,然而自定义的或是应用程序指定的异常处理对象应该继承自ApplicationException。这两种类虽然没有给Exception基类增加任何属性或方法,但它们提供了一种对异常进行分类的有效方法。表4-7列出了一些最常用的系统异常类。例如,数学操作可能抛出ArithmeticException或DivideByZeroException对象,而函数可能抛出ArgumentOutOfRangeException对象。若要获得完整的异常对象列表,可以使用对象浏览器中的搜索命令并寻找Exception子串。表4-7常用的系统异常类
下面介绍上述异常对象共有的一些重要属性和方法(注意,除了Source和HelpLink外,其他所有属性都是只读的)。
Message属性是用来描述异常的文本。例如,DivideByZeroException的Message属性返回字符串“Attemptedtodividebyzero”。Exception对象从System.Object中重写了ToString()方法,并返回一条错误消息,该消息与将在对话框中显示给最终用户的错误消息相同。这一点它与Message属性类似,但它还包括了模块名。如果调试信息已经嵌入到可执行代码中,该方法还会返回过程名和错误发生的准确行号:
System.DivideByZeroException:Attemptedtodividebyzero.atMyApplication.Form1.TestProcinC:\MyApplication\Form1.cs:line70TargetSite属性返回异常第一次抛出时过程的过程名和签名,用C#语言表示:
Int32DivideNumber(Int32x,Int32y)StackTrace属性返回一个字符串,用来描述栈的路径,即从异常最初抛出的位置(也就是说,错误发生的位置)到错误被捕获的位置的路径。例如,假设TestProc过程调用EvalResult过程,后者随后调用DivideNumber函数,并假设后两个过程没有捕获到异常。如果最内部的DivideNumber函数抛出DivideByZeroException,在TestProc过程中读出的StackTrace属性的值将如下所示:
atMyApplication.Form1.DivideNumber(Int32x,Int32y) inC:\MyApplication\Form1.cs:line91atMyApplication.Form1.EvalResult()in C:\MyApplication\Form1.cs:line87atMyApplication.Form1.TestProc()inC:\MyApplication\Form1.cs:line77
只有当可执行文件中包含了调试信息时,才能获得这些详细信息。如果在Release模式下编译程序,将看不到源文件的文件名和行号。显然,StackTrace属性是了解抛出异常时究竟发生了什么的最好途径。
Source属性用来设置或返回抛出异常的组件名。对当前应用程序中抛出的异常,这个属性将返回空的字符串。HelpLink属性设置或返回UniformResourceName(URN)或UniformResourceLocator(URL),这些链接指向有关Exception对象的帮助文件,如下所示:
file://C:/MyApplication/manual.html#ErrorNum42
掌握异常处理机制需要注意四个关键字:try、catch、throw、finally。它们组合成了各种特殊的程序结构,下面我们来讨论这些关键字和结构的用法。4.2.2try和catch块异常处理的中心是try和catch。这两个关键字通常组合在一起使用。try/catch异常处理模块的语法如下:
try{ //程序代码
}catch(Exceptione){ //错误处理代码
}
从上面的语法可以看出,C#异常处理机制将程序代码和错误处理代码分隔开来,程序代码写在try语句块中,错误处理代码写在catch语句块中。catch块负责捕捉try块产生的异常。
例如,图4-4中的除法语句可以放在try块中,而不需要任何的判断语句。当出现除0异常时,将交由catch块处理。
【例4-4】
下面的代码显示了如何使用try和catch语句。
staticvoidMain(){inti1=0,res=0;Console.WriteLine("请输入一个数:");try{i1=int.Parse(Console.ReadLine());res=5/i1;}catch(DivideByZeroExceptione){Console.WriteLine(e.ToString());}Console.WriteLine("res={0}",res.ToString());}
上述代码中,当用户输入字符“0”时,在try结构中将产生一个除0异常,这个异常将会被catch捕捉。如果我们知道确切的异常类型,可以在catch中指明捕捉的是哪种异常。程序运行结果如图4-7所示。图4-7例4-4运行结果
注意:一般来说,我们不在try结构中对变量进行声明,因为此时该变量的有效范围只是块级别的。有时,一个操作可能产生不同类型的异常,如上述代码中,若用户不小心输入字母o,则会产生另一种异常。这时,我们可以使用多个catch块的结构。
try{ //程序代码
}catch(DividedByZeroExceptione){ //错误处理代码1}catch(FormatExceptione){ //错误处理代码2}
那么,有没有一种结构可以捕获所有的异常呢?考虑到异常的层次结构,可以采用下面这种方式。
try{ //程序代码
}catch(Exceptione){ //错误处理代码
}
这种结构称为常规catch块。注意,一个try块只能有一个常规catch块,否则将会出现编译错误。还要注意,在含有常规catch块的多层catch结构中,始终按从最特定到最不特定的顺序对catch块中的异常排序。此方法在将特定异常传递给更常规的catch块之前处理该异常。4.2.3使用throw引发异常前面已经谈到,若在try块中出现异常,系统将隐式抛出异常,并由随后的catch块捕捉并处理。那么,有没有一种语法能将异常显式地抛给程序并进行处理呢?我们可以使用throw关键字。throw命令只有一个参数,即要抛出的异常对象。因此必须创建这类对象并按要求设置它的属性。在大多数情况下,可以创建异常对象,然后在语句中抛出。下面的代码演示了throw的使用。如果用户输入的是1~100范围以外的一个数字,那么就抛出一个自定义的异常。
…//自定义异常
intuserinput=int.Parse(Console.ReadLine());if(userinput>100||userinput<1) thrownewInvalidNumberInputException(userinput+"不是有效输入");
在throw语句中,使用new创建了InvalidNumberInputException对象。请记住,throw抛出对象,而不是异常类型。在这里,InvalidNumberInputException是一个用户自定义异常,也可以直接使用系统提供的异常类型。如果抛出的异常没有被捕获,则程序将会出现致命错误。因此在抛出异常时应确保该异常一定会被捕获。若要捕获所有可能异常的一部分,并把剩下的委派给调用者,throw语句就特别有用。这是非常普遍的编程方式:代码的每个部分都处理它们知道该如何解决的错误,并将其他的错误留给调用代码。正如前面说明过的,如果没有catch表达式捕获当前异常,异常会自动抛给调用者。但使用显式完成抛出动作是好的习惯,因为这样更清楚地表明你是一个严谨的开发人员。下列代码演示了这种编程方式。try{ //程序代码}catch(DividedByZeroExceptione){ //错误处理代码1}catch(FormatExceptione){ //错误处理代码2}catch(Exceptione){ //显式抛出这个未处理的异常给调用者
throw;}
在上面的代码段中,不带参数的throw语句重新抛出当前异常,这种形式的throw语句必须出现在catch结构中才有效。在这种情况下,该语句重新引发当前正由该catch块处理的那个异常并由该语句块的上级调用端捕获。与有参数情况的区别是,后者还重置了异常对象的StackTrace属性(就像创建了一个新的异常)。所以对于重新抛出相同的异常,不带参数更好,因为它能让调用程序正确地确定异常发生位置。
【例4-5】下面的代码演示了如何向上抛出一个异常并进行捕获。
usingSystem;publicclassThrowTest{staticvoidMain(){try{OutSide();}catch(Exceptione){Console.WriteLine(e.StackTrace);}}publicstaticvoidOutSide(){try{InSide();}catch{throw;}}publicstaticvoidInSide(){thrownewException("throwbyInSide");}}
在上面的代码中,Main调用OutSide方法,OutSide方法调用InSide方法,InSide方法抛出一个异常被OutSide捕捉,并使用throw将其重新抛出。这个异常最终由它的上级调用者Main捕获。程序的输出如图4-8所示,注意此时异常的StackTrace属性已经重置了。图4-8异常的StackTrace信息4.2.4自定义异常对象如上一节中的代码所示,可以使用throw命令创建一个新的System.Exception对象(或继承自System.Exception的对象)并设置它的Message属性,但不能实现更进一步的功能。有时需要完整地创建一个新的异常对象,根据 .NET规则,新对象应该继承自System.ApplicationException(与继承自System.SystemException的 .NET运行时环境异常相反)。下面的代码将会展示如何创建继承自System.ApplicationException的类,并用构造函数对异常的Message属性赋值。【例4-6】
下面的代码演示了如何自定义异常对象。
//如何抛出自定义异常
usingSystem; publicclassThrow { staticvoidMain() { try { Console.WriteLine("即将抛出异常"); thrownewmyOwnException("这是我自己的异常"); }catch(myOwnExceptione) { Console.WriteLine(e.Message); } Console.WriteLine("继续执行"); } } publicclassmyOwnException:ApplicationException //自定义异常类
{ publicmyOwnException():base(){} //调用基类无参构造函数
publicmyOwnException(stringmessage):base(message){} //调用基类有参构造函数
}
程序的运行结果如图4-9所示。自定义异常对象除了能报告自定义的错误消息外还有很多其他用途。比如,它们可以包括用来解决错误(或至少尝试这么做)的自定义方法。举例说明:用户可能想设计一个DriveNotReady-Exception类,其中包含一个名为ShowMessage的方法用来显示错误信息,并请求在驱动器中插入光盘然后重试操作。可以将这些代码放在异常类中,使它们的可重用性更好。图4-9例4-6运行结果4.2.5使用finally
异常处理必须考虑的一个问题是,异常的发生可能引起当前方法的中断,使其过早返回。然而,此方法或许已经打开了需要关闭的文件或数据连接,若未及时释放这些资源,将带来一系列问题。C# 提供了一个可选的finally块。如果在该块内指定了语句,无论控制流如何,都会执行这些语句。图4-10说明了其运行流程。图4-10异常的运行流程【例4-7】
下面的代码演示了如何使用finally语句。
staticvoidMain() { intdivident=50; intuserInput; intquotient=0; Console.WriteLine("请输入一个数字:"); try { userInput=Convert.ToInt32(Console.ReadLine()); //将用户输入转换成整数
quotient=divident/userInput; } catch(System.FormatExceptionexcepE) //格式异常{ Console.WriteLine(excepE.Message); } catch(System.DivideByZeroExceptionexcepE) //除0异常
{ Console.WriteLine(excepE.Message); } catch(System.OverflowExceptionexcepE) //溢出异常
{ Console.WriteLine(excepE.Message); }finally { if(quotient!=0) { Console.WriteLine("商为{0}",quotient); } } }
在try块中有两个语句,因为有可能出现下列错误:●用户可能输入字符串;●用户可能输入零;●用户输入的数字超出了int类型的最大范围。所以,编写了三个catch块。如果出现上述异常中的任何一个,程序都会终止,并向用户显示相关的异常信息和出处,说明程序为何关闭。
在finally块中,检查去quotient是否为0。如果得到的结果不为0,就将其显示给用户。程序可能的运行结果如图4-11所示。最后请注意,如果finally语句块中的代码抛出异常,程序则会立即跳出finally语句块,并将异常抛给调用者。因此,必须仔细检查finally代码块中的代码,以保证finally代码在执行时不发生任何错误;如果不能保证没有错误发生,则应该在finally语句块中使用嵌套的try…catch的结构。图4-11例4-7各种可能的运行结果4.3属性属性是类的成员,它们提供了通过访问器读、写或计算私有字段值的灵活机制。如同本书前面一些示例所示,类的创建者往往想要创建一个对象使用者能使用的字段,而保持对该字段的操作控制。例如,可能想要限制该字段的赋值范围。请先研究下面的示例:
usingSystem;classStudent{ publicstringsName; publicstringsAge;}classTest{ publicstaticvoidMain(){ Studentstd1=newStudent(); Console.WriteLine("输入学生姓名:"); std1.sName=Console.ReadLine(); Console.WriteLine("输入学生年龄:"); std1.sAge=Console.ReadLine(); Console.WriteLine("姓名:{0},年龄:{1}",std1.sName,std1.sAge); }}
虽然上述会产生正常的结果,但将类的数据成员公开不是好的编程方法。因为我们在使用用户输入示例的数值前没有进行验证,所以用户可以输入任何值(上例中年龄值不合常理)。如果要实现验证,可以在Student类中编写一个方法,该方法获取字段的值并进行判断。
下面的代码演示了这种方式:
classStudent{ publicstringsName; privatestringsAge; publicvoidSet(stringvalue) { intage=Convert.ToInt16(value); if(age<1||age>150) { sAge="0"; } else { sAge=value; } } publicStringGet() { returnsAge; }}
设置sAge字段的值时,需要调用Set方法,获取该字段的值则需要调用Get方法。显然,为每一个字段编写两个方法是非常繁琐的,很难实现类的封装性和安全性。C#的属性提供了一种更好、更直接的方式,它的基本形式为:
typename{get{//获得域变量的值
}set{//设置域变量的值
}}其中,type指定属性的类型,name指定属性的名称。get访问器和set访问器用于读取和设置字段的值。
【例4-8】
下面的代码演示了属性的创建和使用。
classStudent{ publicstringsName; privatestringsAge; publicstringAge //Age属性封装了sAge { get{returnsAge;} set{sAge=value;} }}classTest{ publicstaticvoidMain() { Studentstd1=newStudent(); std1.sName="Mike"; std1.Age="12"; //设置属性
Console.WriteLine("姓名:{0},年龄:{1}",std1.sName,std1.Age); //读取属性
}}
上面的例子对私有字段sAge进行了属性封装,在测试类中,均通过属性Age进行读取和设置。程序的运行结果如图4-12所示。图4-12例4-8运行结果
分析上面的程序,我们发现,属性由两部分组成:get访问器和set访问器。这里的访问器不是方法,而是用大括号括起来的代码段。属性是对字段的封装,get访问器用于获取字段的值,采用的是return语句;而set访问器则是将外部的值赋给该字段,其中value是关键字,表示外部值。这样,对属性的读/写操作实际上是对字段的操作。图4-13详细地说明了工作过程。图4-13属性的工作过程4.3.1属性的类型属性可以分为以下三种不同的类型。
1.读/写属性读/写属性是既有set访问器又有get访问器的属性。上面例子中的属性均属于这一类型。这种类型的属性同时提供对数据成员的读访问和写访问的能力。
2.只读属性只有get访问器的属性称为只读属性。
【例4-9】
下面的代码演示了只读属性的创建和使用。
publicclassStudent{ privateintaverageScore=70; publicintAverageScore //只读属性
{ get{returnaverageScore;} }}classTest{ staticvoidMain() { Students1=newStudent(); Console.WriteLine("平均分为{0}",s1.AverageScore); //读取属性的值
//s1.AverageScore=88;此行将产生编译错误,试图对只读属性赋值
}}
在该例中,我们创建了类Student的只读属性AverageScore。在Main函数中,实例化该对象后读取该属性值并显示出来。在对该属性赋值时,系统将产生编译错误,原因是AverageScore属性缺少set访问器,无法对其进行写操作。既然只读属性不允许修改,有没有其他的方法可以操作字段的值呢?我们可以使用方法来完成这个任务。请看以下代码段:
…publicclassStudent{ privateintaverageScore=70;publicintAverageScore //只读属性,不能改变字段的值
{ get{returnaverageScore;} } publicvoidChangeScore() //改变字段值的方法
{ averageScore+=10; }}3.只写属性只写属性与只读属性正好相反,这种类型的属性只包含set访问器。因为这种属性没有get访问器,所以只能对其赋值,而不能检索它的值。如果试图检索它的值,编译器就会出现错误。一般认为只写属性是没有意义的,只起到一个对称的作用。所以,我们不再进一步讨论只写属性。
注意:在 .NET 2.0版本中,get和set访问器的访问修饰符可以不同,通常是在保持get访问器可公开访问的情况下,限制set访问器的可访问性。4.3.2属性约束属性有一些重要的约束。第一,因为属性不定义存储位置,所以它不能作为ref参数或out参数传递给方法。第二,用户不能重载属性。可以有两个不同的、但访问相同变量的属性,但这将是异常的。最后,调用get访问器时,属性最好不要改变默认变量的状态,但编译器不强制使用此规则。get操作应该是非侵入性的。由此我们引出属性与字段的比较:
(1)属性是逻辑字段。属性通过get方法和set方法实现对字段的封装。在get方法和set方法中,甚至可以改变原始变量的值。(2)属性是字段的扩展。属性必须和所封装的字段保持相同的返回值。与字段类似,属性也可以附加任何访问修饰符,属性也可以是static的。
(3)与字段不同,属性不直接对应于存储位置。属性与方法不同,既不使用圆括号,也不必指定void,而对实现相似功能的方法而言,则是必须的。4.4索引器索引器是C# 特有的一种访问技术。假设在类中定义了一个数组,在访问数组元素时,传统的做法是在对象名后指定数组名,同时指定要访问元素的索引值,才能进行赋值等操作。用C# 的索引器技术,可以为该数组创建索引器,然后通过从对象直接指定索引的方式来访问数组里的元素。这两种方式从功能上说是等价的。请看下面的代码段:
…stringname1=listBox1.Items[0].ToString() //传统方法
stringname2=listBox1[0].ToString() //索引器方法
…
在上面的代码中,listBox1对象有一个数组成员Items,要获取列表框listBox1的第一个数据项的值,传统的方法如第一行所示。由于使用了索引技术,我们也可以采用第二行的方法直接通过对象索引的方式获取。4.4.1索引器的创建一维索引器的语法如下:
AccessModifierReturnTypethis[DataTypeIndex]{ get{//…} set{//…}}
其中,AccessModifier是访问修饰符。ReturnType是索引的基类型,因此索引访问的每个元素应该是ReturnType类型。参数Index接收被访问元素的下标。理论上,该参数不必是int类型,但是,因为索引通常用来提供数组索引,所以常为整数类型。
与属性一样,索引也定义了两个访问器:get和set。使用索引时,自动调用访问器,且这两个访问器都接收Index作为参数。
【例4-10】
下面的代码演示了索引器的创建和使用。
classSampleCollection<T>{privateT[]arr=newT[100];//泛型数组
publicTthis[inti] //为泛型数组创建泛型索引器
{get{returnarr[i];}set{arr[i]=value;}}}//调用类classProgram{staticvoidMain(string[]args){SampleCollection<string>stringCollection=newSampleCollection<string>();stringCollection[0]="Hello,World"; //使用索引器对其数组成员赋值
System.Console.WriteLine(stringCollection[0]); //使用索引器将数组成员打印出来
}}
上面的代码首先定义了一个泛型类SampleCollection,并为其提供了简单的get和set访问器方法(作为分配和检索值的方法)。注意索引器必须命名为this,因为要访问的索引器必须是它们所属的对象。Program类为存储字符串创建了此类的一个实例,随后一行使用索引器来设置类中数组成员arr的第一个值,接着输出该值。程序最终将显示Hello,World。注意:泛型是 .NET 2.0的一个新功能。泛型将类型参数引入 .NETFramework,使用泛型类型参数,可以编写客户端代码能够使用的单个类,而不会导致运行时强制转换或装箱带来的风险。
声明索引器的步骤如下:
(1)指定索引器的访问修饰符。
(2)说明索引器的返回类型(get访问器的返回类型)。
(3)指定this关键字。
(4)指定索引的数据类型(与数组不同,索引器的索引不一定为整型)。
(5)指定声明索引的变量名。
(6)最后完成get和set访问器的内容,如同定义属性一样。在接下来的例子中,我们来看看如何使用字符串作为索引器的索引。
【例4-11】
下面的代码演示了具有字符串索引的索引器的创建和使用。
classDayCollection{string[]days={"Sun","Mon","Tues","Wed","Thurs","Fri","Sat"};privateintGetDay(stringtestDay){inti=0;foreach(stringdayindays){if(day==testDay){returni;}i++;}return-1;}publicintthis[stringday]{get{return(GetDay(day));}}}classProgram{staticvoidMain(string[]args){DayCollectionweek=newDayCollection();System.Console.WriteLine(week["Fri"]);System.Console.WriteLine(week["Made-upDay"]);}}
在此例中,声明了存储星期几的类,同时声明了一个get访问器,它接受字符串(无名称),并返回相应的整数。例如,星期日将返回0,星期一将返回1,等等。注意,这是一个只读索引,试图对该索引赋值都讲产生错误。最后的输出是:
5-14.4.2多参数索引器在索引器中可以指定一个以上的索引器参数。多个参数意味着要以类似于访问多维数组的方式访问索引器。
【例4-12】
下面的代码演示了多参数索引器的创建和使用。
publicclassArray2D { int[,]a; introws,cols; publicArray2D(intr,intc) //构造函数
{ rows=r; cols=c; a=newint[rows,cols]; }publicintthis[intindex1,intindex2]//二参数索引器
{ get{returna[index1,index2];//返回二维数组的一个元素
set{a[index1,index2]=value;}//对二维数组的一个元素赋值
} } publicclassTest { staticvoidMain() { Array2Darr=newArray2D(2,3); for(inti=0;i<2;i++){ for(intj=0;j<3;j++) { arr[i,j]=i+j; //设置索引器Console.Write(arr[i,j]+""); //显示索引器
} Console.WriteLine(); } } }
在上述示例中,定义的二维索引为该类中的二维数组a进行包装。在读取该数组的值时,完全不用调用数组a(同时a也是私有的,无法调用),而直接采用类名调用即可。该示例的输出结果为:
0121234.5委托什么是委托?在现实生活中,委托就是让别人去代替自己办事。比如你有一份资料要发给客户,而自己不便亲自去做。这时可以派助手小王去给用户发传真,也可以让小王发电子邮件。处理的方法有很多种,小王只需要获得你的授权拿到资料就可以为你办事了。在C#中,可以把委托看做一个方法指针,该指针在不同时刻可以指向不同的方法,并且可以通过该委托执行这些方法。委托如何做到这点呢?在面向对象的程序设计中,委托是能够引用方法的对象。因此,创建委托时,创建的是能够存储方法引用的对象。怎样理解方法的引用?我们知道,引用的本质是内存地址,因此,对象引用就是对象的地址。方法虽然不是对象,它同样在内存中有一个物理位置,而且其入口点就是调用方法时调用的地址。将此地址传给委托,一但委托引用一个方法,就能够通过该委托来调用该方法。例如,假设有两个方法MulFun和AddFun。MulFun接受两个整型参数,返回它们的乘积。AddFun接受两个整型参数,返回它们的和。既然两个方法的参数个数、类型和返回值类型都一样,就可以创建一个委托CalFunDelegate,使其在某个时刻指向MulFun,在某个时刻指向AddFun。如果该委托在指向MulFun时调用,则将对委托中传递的参数做乘法计算;如果该委托在指向AddFun时调用,则将对委托中传递的参数做加法计算。4.5.1定义委托定义委托的语法如下:
AccessModifierdelegateReturnTypeDelegateName(parameter-list);其中,AccessModifier是访问修饰符,Delegate是委托关键字,ReturnType和parameter-list是委托所引用方法的返回值类型和参数列表。这里应注意,定义委托和定义方法非常类似,只是没有定义委托的实现,因此委托的语法以分号结束。
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 2025年度弃土场租赁合同简易版(矿山开采辅助设施)3篇
- 二零二五年度房产交易税费代缴合同2篇
- 二零二五年度手车交易不过户车辆交易区块链技术合同3篇
- 2025年度推拿按摩师专业设备租赁服务协议3篇
- 二零二五年度廉洁采购协议:国有企业物资采购合同3篇
- 2025年度手摩托车专业维修服务买卖协议3篇
- 二零二五年度影楼财务与审计人员劳动合同3篇
- 2025年度电子信息设备可靠性试验技术服务合同3篇
- 二零二五年度云计算平台技术服务费合同3篇
- 2024年生物制药技术进口与许可合同
- JJF 2184-2025电子计价秤型式评价大纲(试行)
- 排污许可证办理合同1(2025年)
- 上海科目一考试题库参考资料1500题-上海市地方题库-0
- 军工合作合同范例
- 【7地XJ期末】安徽省宣城市宁国市2023-2024学年七年级上学期期末考试地理试题(含解析)
- 2025年中国稀土集团总部部分岗位社会公开招聘管理单位笔试遴选500模拟题附带答案详解
- 超市柜台长期出租合同范例
- 设备操作、保养和维修规定(4篇)
- 广东省广州市2025届高三上学期12月调研测试语文试题(含答案)
- 【8物(科)期末】合肥市第四十五中学2023-2024学年八年级上学期期末物理试题
- 统编版2024-2025学年三年级语文上册期末学业质量监测试卷(含答案)
评论
0/150
提交评论