大学C语言程序设计-李继武 彭德林-课件PPT
收藏
资源目录
压缩包内文档预览:(预览前20页/共36页)
编号:21836185
类型:共享资源
大小:6.11MB
格式:ZIP
上传时间:2019-09-06
上传人:QQ24****1780
认证信息
个人认证
王**(实名认证)
浙江
IP属地:浙江
25
积分
- 关 键 词:
-
大学C语言程序设计-李继武
彭德林-课件PPT
大学
语言程序设计
李继武
彭德林
课件
ppt
- 资源描述:
-
大学C语言程序设计-李继武 彭德林-课件PPT,大学C语言程序设计-李继武,彭德林-课件PPT,大学,语言程序设计,李继武,彭德林,课件,ppt
- 内容简介:
-
第4章 C#面向对象高级编程 前面介绍了面向对象程序设计的基本概念和应用。但是面向对象还包括很多其他重要的概念。本章将深入分析面向对象编程的概念,并详细说明利用工具进行面向对象程序设计的方法。 4.1 类的继承与多态4.1.1 继承1、概述现实世界中的许多实体之间不是相互孤立的,它们往往具有共同的特征,也存在内在的差别。人们可以采用层次结构来描述这些实体之间的相似之处和不同之处。为了用软件语言对现实世界中的层次结构进行模型化,面向对象的程序设计技术引入了继承的概念。一个类从另一个类派生出来时,派生类从基类那里继承特性。派生类也可以作为其它类的基类。从一个基类派生出来的多层类形成了类的层次结构。注意:C#中,派生类只能从一个类中继承。这是因为,在C+中,人们在大多数情况下不需要一个从多个类中派生的类。从多个基类中派生一个类,这往往会带来许多问题,从而抵消了这种灵活性带来的优势。C#中,派生类从它的直接基类中继承成员:方法、域、属性、事件、索引指示器。除了构造函数和析构函数,派生类隐式地继承了直接基类的所有成员。C#中,派生类从它的直接基类中继承成员:方法、域、属性、事件、索引指示器。除了构造函数和析构函数,派生类隐式地继承了直接基类的所有成员。程序清单:using System;namespace ConsoleApplication1class Vehicle /定义汽车类int wheels; /公有成员:轮子个数protected float weight; /保护成员:重量public Vehicle();public Vehicle(int w, float g) wheels=w;weight=g;public void Speak()Console.WriteLine(the w vehicle is speaking!);class Car: Vehicle /定义轿车类:从汽车类中继承 int passengers;/私有成员:乘客数public Car(int w, float g, int p) : base(w,g) wheels=w;weight=g;passengers=p;Vehicle作为基类,体现了“汽车”这个实体具有的公共性质:汽车都有轮子和重量。 Car类继承了Vehicle的这些性质,并且添加了自身的特性:可以搭载乘客。 C#中的继承符合下列规则:(1)继承是可传递的。如果C从B中派生,B又从A中派生,那么C不仅继承了B中声明的成员,同样也继承了A中的成员。Object类作为所有类的基类。(2)派生类应当是对基类的扩展。派生类可以添加新的成员,但不能除去己经继承的成员的定义。(3)构造函数和析构函数不能被继承。除此以外的其它成员,不论对它们定义了怎样的访问方式,都能被继承。基类中成员的访问方式只能决定派生类能否访问它们。(4)派生类如果定义了与继承而来的成员同名的新成员,就可以覆盖己继承的成员。但这并不因为这派生类删除了这些成员,只是不能再访问这些成员。(5)类可以定义虚方法、虚属性以及虚索引指示器,它的派生类能够重载这些成员,从而实现类可以展示出多态性。2、覆盖 我们上面提到,类的成员声明中,可以声明与继承而来的成员同名的成员。这时我们称派生类的成员覆盖(hide)了基类的成员。这种情况下,编译器不会报告错误,但会给出一个警告。对派生类的成员使用new关键字,可以关闭这个警告。 前面汽车类的例子中,类Car继承了Vehicle的Speak()方法。我们可以给Car类也声明一个Speak()方法,覆盖Vehicle中的Speak,见下面的代码。 程序清单: using System; namespace ConsoleApplication1 class Vehicle/定义汽车类public int wheels; /公有成员:轮子个数protected float weight;/保护成员:重量public Vehicle();public Vehicle(int w, float g) wheels=w; weight=g;public void Speak() Console.WriteLine(the w vehicle is speaking!);class Car:Vehicle /定义轿车类int passengers;/私有成员:乘客数public Car(int w, float g, int p)wheels=w;weight=g; passengers=p;new public void Speak()Console.WriteLine(Di-di!); 注意:如果在成员声明中加上了new关键字修饰,而该成员事实上并没有覆盖继承的成员,编译器将会给出警告。在个成员声明同时使用new和override则编译器会报告错误。3、base保留字base关键字主要是为派生类调用基类成员提供一个简写的方法。我们先看一个例子程序代码:using System;namespace ConsoleApplication1class Apublic void F()/F的具体执行代码 public int thisint nIndex getsetclass Bpublic void G()int x=base0;base.F();类B从类A中继承,B的方法G中调用了A的方法F和索引指示器。方法F在进行编译时等价于:public void G() int x=(A (this)0; (A (this).F();使用base关键字对基类成员的访问格式为:base .标识符base 表达式列表 4.1.2 多态 在面向对象的系统中,多态性是一个非常重要的概念,它允许客户对一个对象进行操作,由对象来完成一系列的动作,具体实现哪个动作、如何实现由系统负责解释。 C#中的多态“多态性”一词最早用于生物学,指同一种族的生物体具有相同的特性。在C#中,多态性的定义是:同一操作作用于不同的类的实例,不同的类将进行不同的解释,最后产生不同的执行结果。C#支持两种类型的多态性:(1)编译时的多态性编译时的多态性是通过重载来实现的。我们在前面介绍了方法重载,都实现了编译时的多态性。对于非虚的成员来说,系统在编译时,根据传递的参数、返回的类型等信急决定实现何种操作。(2)运行时的多态性运行时的多态性就是指直到系统运行时,才根据实际情况决定实现何种操作。C#中,运行时的多态性通过虚成员实现。编译时的多态性为我们提供了运行速度快的特点,而运行时的多态性则带来了高度灵活和抽象的特点。 虚方法 当类中的方法声明前加上了virtual修饰符,我们称之为虚方法,反之为非虚。使用了virtual修饰符后,不允许再有static, abstract,或override修饰符。对于非虚的方法,无论被其所在类的实例调用,还是被这个类的派生类的实例调用,方法的执行方式不变。而对于虚方法,它的执行方式可以被派生类改变,这种改变是通过方法的重载来实现的。案例:虚方法与非虚方法的调用目标:说明了虚方法与非虚方法的区别步骤:1、启动VS.NET,新建一个控制台应用程序,名称填写为“DifferentiateTest”,位置设置为“c:CSharpSampleschp4”。2、在代码设计窗口中编辑Class1.cs。其中的代码编写如下: using System;namespace DifferentiateTest class A public void F()Console.WriteLine(A.F);public virtual void G()Console.WriteLine(A.G);class B: A new public void F()Console.WriteLine(B.F);public override void G()Console.WriteLine(B.G); class Teststatic void Main()B b=new B();A a=b;a.F();b.F();a.G();b.G();3、按Ctrl + F5编译并运行该程序,效果如图5-2所示。例子中,A类提供了两个方法:非虚的F和虚方法G。类B则提供了一个新的非虚的方法F,从而覆盖了继承的F;类B同时还重载了继承的方法G。在本例中,方法a.G()实际调用了B.G,而不是A.G。这是因为编译时值为A,但运行时值为B,所以B完成了对方法的实际调用。 在派生类中对虚方法进行重载 先让我们回顾一下普通的方法重载。普通的方法重载指的是:类中两个以上的方法(包括隐藏的继承而来的方法),取的名字相同,只要使用的参数类型或者参数个数不同,编译器便知道在何种情况下应该调用哪个方法。 而对基类虚方法的重载是函数重载的另一种特殊形式。在派生类中重新定义此虚函数时,要求的是方法名称、返回值类型、参数表中的参数个数、类型、顺序都必须与基类中的虚函数完全一致。在派生类中声明对虚方法的重载,要求在声明中加上override关键字,而且不能有new, static或virtual修饰符。4.1.3 抽象和密封1、抽象类 有时候,基类并不与具体的事物相联系,而是只表达一种抽象的概念,用以为它的派生类提供一个公共的界面。为此,C#中引入了抽象类(abstract class)的概念。注意:C+程序员在这里最容易犯错误。C+中没有对抽象类进行直接声明的方法,而认为只要在类中定义了纯虚函数,这个类就是一个抽象类。纯虚函数的概念比较晦涩,直观上不容易为人们接受和掌握,因此C#抛弃了这一概念。 抽象类使用abstract修饰符,对抽象类的使用有以下几点规定: (1)抽象类只能作为其它类的基类,它不能直接被实例化,而且对抽象类不能使用new操作符。抽象类如果含有抽象的变量或值,则它们要么是null类型,要么包含了对非抽象类的实例的引用。 (2)抽象类允许包含抽象成员,虽然这不是必须的。 (3)抽象类不能同时又是密封的。 如果一个非抽象类从抽象类中派生,则其必须通过重载来实现所有继承而来的抽象成员。请看下面的示例:using System;namespace ConsoleApplication1abstract class Apublic abstract void F();abstract class B: Apublic void G()class C: Bpublic override void FD/F的具体实现代码 抽象类A提供了一个抽象方法F。类B从抽象类A中继承,并且又提供了一个方法G;因为B中并没有包含对F的实现,所以B也必须是抽象类。类C从类B中继承,类中重载了抽象方法F,并且提供了对F的具体实现,则类C允许是非抽象的。 2、抽象方法由于抽象类本身表达的是抽象的概念,因此类中的许多方法并不一定要有具体的实现,而只是留出一个接日来作为派生类重载的界面。举一个简单的例子,“图形”这个类是抽象的,它的成员方法“计算图形面积”也就没有实际的意义。面积只对“图形”的派生类比如“圆”、“一角形”这些非抽象的概念才有效,那么我们就可以把基类“图形”的成员方法“计算面积”声明为抽象的,具体的实现交给派生类通过重载来实现。一个方法声明中如果加上abstract修饰符,我们称该方法为抽象方法(abstract method )。如果一个方法被声明也是抽象的,那么该方法默认也是一个虚方法。事实上,抽象方法是一个新的虚方法,它不提供具体的方法实现代码。我们知道,非虚的派生类要求通过重载为继承的虚方法提供自己的实现,而抽象方法则不包含具体的实现内容,所以方法声明的执行体中只有一个分号“;”。只能在抽象类中声明抽象方法。对抽象方法,不能再使用static或virtual修饰符,而且方法不能有任何可执行代码,哪怕只是一对大括号中间加一个一个分号“;”都不允许出现,只需要给出方法的原型就可以了。 还要注意,抽象方法在派生类中不能使用base关键字来进行访问。例如,下面的代码在编译时会发生错误: using System; namespace ConsoleApplication1 class A public abstract void F(); class B: A public override void F()base.F();/错误,base.F是抽象方法 我们还可以利用抽象方法来重载基类的虚方法,这时基类中虚方法的执行代码就被“拦截”了。下面的例子说明了这一点:using System;namespace ConsoleApplication1class A public virtual void F()Console.WriteLine(A.F);abstract class B: Apublic abstract override void F();class C: Bpublic override void F()Console.WriteLine(C.F);类A声明了一个虚方法F,派生类B使用抽象方法重载了F,这样B的派生类C就可以重载F并提供自己的实现。3、密封类 想想看,如果所有的类都可以被继承,继承的滥用会带来什么后果?类的层次结构体系将变得十分庞大,类之间的关系杂乱无章,对类的理解和使用都会变得十分困难。有时候,我们并不希望自己编写的类被继承。另一些时候,有的类己经没有再被继承的必要。C#提出了一个密封类(sealed class)的概念,帮助开发人员来解决这一问题。 密封类在声明中使用sealed修饰符,这样就可以防止该类被其它类继承。如果试图将一个密封类作为其它类的基类,C#将提不出错。理所当然,密封类不能同时又是抽象类,因为抽象总是希望被继承的。在哪些场合下使用密封类呢?密封类可以阻止其它程序员在无意中继承该类,而且密封类可以起到运行时优化的效果。实际上,密封类中不可能有派生类,如果密封类实例中存在虚成员函数,该成员函数可以转化为非虚的,函数修饰符virtual不再生效。让我们看下面的例子: using System; namespace ConsoleApplication1 abstract class A public abstract void F(); sealed class B: A public override void F()/F的具体实现代码如果我们尝试写下面的代码:class C: B C#会指出这个错误,告诉你B是一个密封类,不能试图从B中派生任何类。4、密封方法我们己经知道,使用密封类可以防止对类的继承。C#还提出了密封方法(sealed method)的概念,以防止在方法所在类的派生类中对该方法的重载。对方法可以使用sealed修饰符,这时我们称该方法是一个密封方法。不是类的每个成员方法都可以作为密封方法,密封方法必须对基类的虚方法进行重载,提供具体的实现方法。所以,在方法的声明中,sealed修饰符总是和override修饰符同时使用。请看例子代码。程序清单:using System;namespace ConsoleApplication1class Apublic virtual void F() Console.WriteLine(A.F); public virtual void G() Console.WriteLine(A.G);class B: A sealed override public void F() Console.WriteLine(B.F);override public void G() Console.WriteLine(B.G);class C: Boverride public void G()Console.WriteLine(C.G); 类B对基类A中的两个虚方法均进行了重载,其中F方法使用了Sealed修饰符,成为一个密封方法。G方法不是密封方法,所以在B的派生类C中,可以重载方法G,但不能重载方法F。 4.2 操作符重载4.2.1 问题的提出在面向对象的程序设计中,自己定义一个类,就等于创建了一个新类型。类的实例和变量一样,可以作为参数传递,也可以作为返回类型。在前几章中,我们介绍了系统定义的许多操作符。比如对于两个整型变量,使用算术操作符可以简便地进行算术运算:class Apublic int x;public int y;public int Plus()return x+y;再比如,我们希望将属于不同类的两个实例的数据内容相加:class B public int x;class Test public int z;public static void Main()A a=new A();B b=new B();z=a.x+b.x;使用a.x+b.x这种写法不够简洁,也不够直观。更为严重的问题是,如果类的成员在声明时使用的不是public修饰符的话,这种访问就是非法的。我们知道,在C#中,所有数据要么属于某个类,要么属于某个类的实例,充分体现了面向对象的思想。因此,为了表达上的方便,人们希望可以重新给己定义的操作符赋予新的含义,在特定的类的实例上进行新的解释。这就需要通过操作符重载来解决。4.2.2 使用成员方法重载操作符 C#中,操作符重载总是在类中进行声明,并且通过调用类的成员方法来实现。 操作符重载声明的格式为:type operator operator-game (formal-param-list)C#中,下列操作符都是可以重载的: + - ! + - true false * / % & | = != = =但也有一些操作符是不允许进行重载的,如: =,&,|,?:,new, typeof , sizeof, is一元操作符重载顾名思义,一元操作符重载时操作符只作用于一个对象,此时参数表为空,当前对象作为操作符的单操作数。下面我们举一个角色类游戏中经常遇到的例子。扮演的角色具有内力、体力、经验值、剩余体力、剩余内力五个属性,每当经验值达到一定程度时,角色便会升级,体力、内力上升,剩余体力和内力补满。“升级”我们使用重载操作符“+”来实现。案例:游戏中“升级”问题目标:掌握一元操作符重载的基本方法步骤:1、启动VS.NET,新建一个控制台应用程序,名称填写为“PlayerTest”,位置设置为“c:CSharpSampleschp4”。2、在代码设计窗口中编辑Class1.cs。其中的代码编写如下: using System;namespace PlayerTest class Player public int neili;public int tili;public int jingyan;public int neili_r;public int tili_r;public Player() neili=10;tili=50;jingyan=0;neili_r=50;tili_r=50;public static Player operator +(Player p) p.neili=p.neili+50; p.tili=p.tili+100; p.neili_r=p.neili; p.tili_r=p.tili; return p;public void Show() Console.WriteLine(Tili:0 ,tili);Console.WriteLine(Jingyan: 0,jingyan);Console.WriteLine(Neili0,neili);Console.WriteLine(Tili_full: 0,tili_r);Console.WriteLine(Neili_full: 0,neili_r);class Testpublic static void Main()Player man=new Player();man.Show();man+;Console.WriteLine(Now upgrading:);man. Show();3、按Ctrl + F5编译并运行该程序,效果如图4-2所示。二元操作符重载 大多数情况下我们使用二元操作符重载。这时参数表中有一个参数,当前对象作为该操作符的左操作数,参数作为操作符的右操作数。下面我们给出二元操作符重载的一个简单例子。案例:笛卡儿坐标相加问题目标:掌握二元操作符重载的基本方法步骤:1、启动VS.NET,新建一个控制台应用程序,名称填写为“DKRTest”,位置设置为“c:CSharpSampleschp4”。2、在代码设计窗口中编辑Class1.cs。其中的代码编写如下:using System;namespace DKRTest class DKR public int x,y,z;public DKR(int vx,int vy, int vz) x=vx; y=vy; z=vz; public static DKR operator +(DKR d1,DKR d2) DKR dkr=new DKR(0,0,0);dkr.x=d1.x+d2.x;dkr.y=d1.y+d2.y;dkr.z=d1.z+d2.z;return dkr;class Test public static void Main()DKR d1=new DKR(3,2,1);DKR d2=new DKR(0,6,5);DKR d3=d1+d2;Console.WriteLine(The 3d location of d3 is: 0, 1, 2,d3.x,d3.y,d3.z);3、按Ctrl + F5编译并运行该程序,效果如图4-3所示 4.3 类型转换 转换是使一种类型的表达式可以被视为另一种类型。转换可以是隐式或显式,这将确定是否需要显式地强制转换。例如,从int类型到long类型的转换是隐式的,因此int类型的表达式可隐式地按long类型处理。从long类型到int类型的反向转换是显式的,因此需要显式地强制转换。 int a = 123; long b = a; / 从 int 类型到 long 类型的转换 int c = (int) b; / 从 long 类型到 int 类型的反向转换4.3.1 隐式类型转换下列转换属于隐式转换:标识转换 隐式数值转换 隐式枚举转换 隐式常数表达式转换 用户定义的隐式转换 隐式转换可以在各种情况下发生,包括函数成员调用、强制转换表达式以及赋值。1、标识转换是在同一类型(可为任何类型)内进行转换。这种转换的存在,仅仅是为了使已具有所需类型的实体可被认为是可转换的(转换为该类型)。2、隐式数值转换为: 从sbyte到short、int、long、float、double或decimal。从byte到short、ushort、int、uint、long、ulong、float double或decimal。从short到int、long、float、double或decimal。从ushort到int、uint、long、ulong、float、double或decimal。从int到long、float、double或decimal。从uint到long、ulong、float、double或decimal。从long到float、double或decimal。从ulong到float、double或decimal。从char到ushort、int、uint、long、ulong、float、double或decimal。从float到double。 从int、uint、long或ulong到float以及从long或ulong到double的转换可能导致精度损失,但决不会影响到它的数量级。其他的隐式数值转换决不会丢失任何信息。不存在向char类型的隐式转换,因此其它整型的值不会自动转换为char类型。3、隐式枚举转换允许将十进制整数0转换为任何枚举类型。4、隐式常数表达式转换允许进行以下转换: int类型的常数表达式可以转换为sbyte、byte、short、ushort、uint或ulong类型(前提是常数表达式的值在目标类型的范围内)。 long类型的常数表达式可以转换为ulong类型(前提是常数表达式的值不为负)。5、用户定义的隐式转换由以下三部分组成:先是一个标准的隐式转换(可选);然后是执行用户定义的隐式转换运算符,最后是另一个标准的隐式转换(可选)。计算用户定义的转换的精确规则的说明。从S类型到T类型的用户定义的隐式转换按下面这样处理:1)查找类型集D,将从该类型集考虑用户定义的转换运算符。此集合由S(如果S是类或构造)、S的基类(如果S是类)和T(如果T是类或结构)组成。 2)查找适用的用户定义转换运算符集合U。集合U由用户定义的隐式转换运算符组成,这些运算符是在D中的类或结构内声明的,用于从包含S的类型转换为被T包含的类型。如果U为空,则转换未定义并且发生编译时错误。3)在U中查找运算符的最精确的源类型SX:如果U中的所有运算符都从S转换,则SX为S。否则,SX在U中运算符的合并目标类型集中是被包含程度最大的类型。如果找不到这样的被包含程度最大的类型,则转换是不明确的,并且发生编译时错误。 4)在U中查找运算符的最精确的目标类型TX:如果U中的所有运算符都转换为T,则TX为T。否则,TX在U中运算符的合并目标类型集中是包含程度最大的类型。如果找不到这样的包含程度最大的类型,则转换是不明确的,并且发生编译时错误。5)如果U中正好含有一个从SX转换到TX的用户定义转换运算符,则这就是最精确的转换运算符。如果不存在此类运算符,或者如果存在多个此类运算符,则转换是不明确的,并且发生编译时错误。否则,将应用用户定义的转换:如果S不是SX,则先执行一个从S到SX的标准隐式转换。调用最精确的用户定义转换运算符,以从SX转换到TX。如果TX不是T,则再执行一个TX到T的标准隐式转换。4.3.2显式类型转换下列转换属于显式转换:显式数值转换显式枚举转换用户定义的显式转换显式转换可在强制转换表达式中出现。显式转换集包括所有隐式转换。这意味着允许使用冗余的强制转换表达式。 不是隐式转换的显式转换是这样的一类转换:它们不能保证总是成功,知道有可能丢失信息,变换前后的类型显著不同,以至值得使用显式表示法。1、显式数值转换是指从一个数值类型到另一个数值转换的转换,此转换不能用已知的隐式数值转换实现,它包括:从sbyte到byte、ushort、uint、ulong或char。从byte到sbyte 和char。从short到sbyte、byte、ushort、uint、ulong或char。从ushort到sbyte、byte、short或char。从int到sbyte、byte、short、ushort、uint、ulong或char。从uint到sbyte、byte、short、ushort、int或char。从long到sbyte、byte、short、ushort、int、uint、ulong或char。从ulong到sbyte、byte、short、ushort、int、uint、long或char。从char到sbyte、byte或short。从float到sbyte、byte、short、ushort、int、uint、long、ulong、char或decimal。从double到sbyte、byte、short、ushort、int、uint、long、ulong、char、float或decimal。从decimal到sbyte、byte、short、ushort、int、uint、long、ulong、char、float或double。 由于显式转换包括所有隐式和显式数值转换,因此总是可以使用强制转换表达式从任何数值类型转换为任何其他的数值类型。 显式数值转换有可能丢失信息或导致引发异常。显式数值转换按下面所述处理:对于从一个整型到另一个整型的转换,处理取决于该转换发生时的溢出检查上下文:1)在checked上下文中,如果源操作数的值在目标类型的范围内,转换就会成功,但如果源操作数的值在目标类型的范围外,则会引发System.OverflowException。2)在unchecked上下文中,转换总是会成功并按下面所述进行。 如果源类型大于目标类型,则截断源值(截去源值中容不下的最高有效位)。然后将结果视为目标类型的值。 如果源类型小于目标类型,则源值或按符号扩展或按零扩展,以使它的大小与目标类型相同。如果源类型是有符号的,则使用按符号扩展;如果源类型是无符号的,则使用按零扩展。然后将结果视为目标类型的值。 如果源类型的大小与目标类型相同,则源值被视为目标类型的值。对于从decimal到整型的转换,源值向零舍入到最接近的整数值,该整数值成为转换的结果。如果转换得到的整数值不在目标类型的范围内,则会引发System.OverflowException。对于从float或double到整型的转换,处理取决于发生该转换时的溢出检查上下文:1)在checked上下文中,如下所示进行转换:如果操作数的值是NaN或无穷大,则引发System.OverflowException。 否则,源操作数会向零舍入到最接近的整数值。如果该整数值处于目标类型的范围内,则该值就是转换的结果。 否则,引发System.OverflowException。2)在unchecked上下文中,转换总是会成功并按下面这样继续。 如果操作数的值是NaN或infinite,则转换的结果是目标类型的一个未经指定的值。 否则,源操作数会向零舍入到最接近的整数值。如果该整数值处于目标类型的范围内,则该值就是转换的结果。否则,转换的结果是目标类型的一个未经指定的值。对于从double到float的转换,double值舍入到最接近的float值。如果double值过小,无法表示为float值,则结果变成正零或负零。如果double值过大,无法表示为float 值,则结果变成正无穷大或负无穷大。如果double 值为 NaN,则结果仍然是NaN。 对于从float或double到decimal的转换,源值转换为用decimal形式来表示,并且在需要时,将它在第28位小数位数上舍入到最接近的数字。如果源值过小,无法表示为decimal,则结果变成零。如果源值为 NaN、无穷大或者太大而无法表示为decimal值,则将引发System.OverflowException。对于从decimal到float或double的转换,decimal值舍入到最接近的double或float值。虽然这种转换可能会损失精度,但决不会导致引发异常。 2、显式枚举转换为:从sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double或decimal 到任何枚举类型。 从任何枚举类型到sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double或decimal。 从任何枚举类型到任何其他枚举类型。两种类型之间的显式枚举转换是通过将任何参与的枚举类型都按该枚举类型的基础类型处理,然后在结果类型之间执行隐式或显式数值转换。例如,给定具有int基础类型的枚举类型E,从E到byte的转换按从int到byte的显式数值转换处理,而从byte到E的转换按从byte到int的隐式数值转换处理。3、用户定义的显式转换由以下三个部分组成:先是一个标准的显式转换(可选),然后是执行用户定义的隐式或显式转换运算符,最后是另一个标准的显式转换(可选)。从S类型到T类型的用户定义的显式转换按下面这样处理: 1)查找类型集D,将从该类型集考虑用户定义的转换运算符。该类型集由S(如果S为类或结构)、S的基类(如果S为类)、T(如果T为类或结构)和T的基类(如果T为类)组成。2)查找适用的用户定义转换运算符集合U。集合U由用户定义的隐式或显式转换运算符组成,这些运算符是在D中的类或结构内声明的,用于从包含S或被S包含的类型转换为包含T或被T包含的类型。如果U为空,则转换未定义并且发生编译时错误。3)在U中查找运算符的最精确的源类型SX:如果U中的所有运算符都从S转换,则SX为S。否则,如果U中的所有运算符都从包含S的类型转换,则SX在这些运算符的合并源类型集中是被包含程度最大的类型。如果找不到最直接包含的类型,则转换是不明确的,并且发生编译时错误。否则,SX在U中运算符的合并源类型集中是包含程度最大的类型。如果找不到这样的包含程度最大的类型,则转换是不明确的,并且发生编译时错误。4)在U中查找运算符的最精确的目标类型TX:如果U中的所有运算符都转换为T,则TX为T。否则,如果U中的所有运算符都转换为被T包含的类型,则TX在这些运算符的合并源类型集中是包含程度最大的类型。如果找不到这样的包含程度最大的类型,则转换是不明确的,并且发生编译时错误。否则,TX在U中运算符的合并目标类型集中是被包含程度最大的类型。如果找不到这样的被包含程度最大的类型,则转换是不明确的,并且发生编译时错误。5)如果U中正好含有一个从SX转换到TX的用户定义转换运算符,则这就是最精确的转换运算符。如果不存在此类运算符,或者如果存在多个此类运算符,则转换是不明确的,并且发生编译时错误。否则,将应用用户定义的转换:如果S不是SX,则先执行一个从S到SX的标准显式转换。调用最精确的用户定义转换运算符,以从SX转换到TX。如果TX不是T,则再执行一个从TX到T的标准显式转换。标准显式转换包括所有的标准隐式转换以及一个显式转换的子集,该子集是由那些与已知的标准隐式转换反向的转换组成的。换言之,如果存在一个从A类型到B类型的标准隐式转换,则一定存在与其对应的两个标准显式转换(一个是从A类型到B类型,另一个是从B类型到A类型)。4.3.3类的引用转换1、隐式引用转换为:从任何引用类型到object。从任何类类型S到任何类类型T(前提是S是从T派生的)。从任何类类型S到任何接口类型T(前提是S实现了T)。从任何接口类型S到任何接口类型T(前提是S是从T派生的)。从元素类型为SE的数组类型S到元素类型为TE的数组类型T(前提是以下所列的条件均为真): 1)S和T只是元素类型不同。换言之,S和T具有相同的维数。2)SE和TE都是引用类型。3)存在从SE到TE的隐式引用转换。从任何数组类型到System.Array。从任何委托类型到System.Delegate。从null类型到任何引用类型。隐式引用转换是指一类引用类型之间的转换,这种转换总是可以成功,因此不需要在运行时进行任何检查。引用转换无论是隐式的还是显式的,都不会更改所转换的对象的引用标识。换言之,虽然引用转换可能改变该引用的类型,但决不会更改所引用对象的类型或值。2、显式引用转换为: 从object到任何其他引用类型。从任何类类型S到任何类类型T(前提是S为T的基类)。从任何类类型S到任何接口类型T(前提是S未密封并且S不实现T)。从任何接口类型S到任何类类型T(前提是T未密封或T实现S)。从任何接口类型S到任何接口类型T(前提是S不是从T派生的)。从元素类型为SE的数组类型S到元素类型为TE的数组类型T(前提是以下所列条件均为真):1)S和T只是元素类型不同。换言之,S和T具有相同的维数。2)SE和TE都是引用类型。3)存在从SE到TE的显式引用转换。从System.Array以及它实现的接口到任何数组类型。从System.Delegate以及它实现的接口到任何委托类型。显式引用转换是那些需要运行时检查以确保它们正确的引用类型之间的转换。为了使显式引用转换在运行时成功,源操作数的值必须为null,或源操作数所引用的对象的实际类型必须是一个可通过隐式引用转换转换为目标类型的类型。如果显式引用转换失败,则将引发System.InvalidCastException。引用转换无论是隐式的还是显式的,都不会更改被转换的对象的引用标识。换言之,虽然引用转换可能更改引用的类型,但决不会更改所引用对象的类型或值。4.3.4装箱与拆箱装箱转换允许将值类型隐式转换为引用类型。将值类型的一个值装箱包括以下操作:分配一个对象实例,然后将值类型的值复制到该实例中。装箱转换允许将“值类型”隐式转换为“引用类型”。存在下列装箱转换:从任何“值类型”(包括任何“枚举类型”)到类型object。从任何“值类型”(包括任何“枚举类型”)到类型System.ValueType。从任何“值类型”到“值类型”实现的任何“接口类型”。从任何“枚举类型”到System.Enum类型。将“值类型”的值装箱的操作包括:分配一个对象实例并将“值类型”的值复制到该实例中。最能说明“值类型”的值的实际装箱过程的办法是,设想有一个为该类型设置的装箱类。对任何“值类型”的T而言,装箱类的行为可用下列声明来描述:sealed class T_Box: System.ValueTypeT value;public T_Box(T t) value = t;T类型值v的装箱过程现在包括执行表达式new T_Box(v)和将结果实例作为object类型的值返回。因此,下面的语句:int i = 123;object box = i;在概念上相当于:int i = 123;object box = new int_Box(i);实际上,像上面这样的T_Box和int_Box并不存在,并且装了箱的值的动态类型也不会真的属于一个类类型。相反,T类型的装了箱的值属于动态类型T,若用is运算符来检查动态类型的话,也仅能引用类型T。例如,int i = 123;object box = i;if (box is int) Console.Write(Box contains an int);将在控制台上输出字符串“Box contains an int”。装箱转换隐含着复制一份欲被装箱的值。这不同于从引用类型到 object 类型的转换,在后一种转换中,转换后的值继续引用同一实例,只是将它当作派生程度较小的object类型而已。例如,设有下列的声明:struct Point public int x, y; public Point(int x, int y) this.x = x; this.y = y; 则下面的语句:Point p = new Point(10, 10);object box = p;p.x = 20;Console.Write(Point)box).x);将在控制台上输出值10,因为将p赋值给box是一个隐式装箱操作,它将复制p的值。如果将Point声明为class,由于p和box将会引用同一个实例,因此输出值为20。拆箱也称为取消装箱转换。取消装箱转换允许将引用类型显式转换为值类型。一个取消装箱操作包括以下两个步骤:首先检查对象实例是否为给定值类型的一个装了箱的值,然后将该值从实例中复制出来。取消装箱转换允许将引用类型显式转换为值类型。存在以下取消装箱转换:从类型 object 到任何值类型(包括任何枚举类型)。 从类型 System.ValueType 到任何值类型(包括任何枚举类型)。 从任何接口类型到实现了该接口类型的任何值类型。 从 System.Enum 类型到任何枚举类型。 取消装箱操作包括以下两个步骤:首先检查该对象实例是否是某个给定的值类型的装了箱的值,然后将值从实例中复制出来。参照前面关于装箱类的描述,从对象box到值类型T的取消装箱转换相当于执行表达式 (T_Box)box).value。因此,下面的语句:object box = 123;int i = (int)box;在概念上相当于:object box = new int_Box(123);int i = (int_Box)box).value;为使到给定值类型的取消装箱转换在运行时取得成功,源操作数的值必须是对某个对象的引用,而该对象先前是通过将该值类型的某个值装箱而创建的。如果源操作数为null,则将引发System.NullReferenceException。如果源操作数是对不兼容对象的引用,则将引发System.InvalidCastException。4.4 结构和接口4.4.1 结构结构与类有很多相似之处:结构可以实现接口,并且可以具有与类相同的成员类型。然而,结构在几个重要方面不同于类:结构为值类型而不是引用类型,并且结构不支持继承。结构的值存储在“在堆栈上”或“内联”。细心的程序员有时可以通过聪明地使用结构来增强性能。声明一个结构,它有三个成员:一个属性、一个方法和一个私有字段,创建该结构的一个实例,并将其投入使用:案例:声明一个结构目标:了解声明结构的基本方法步骤:1、启动VS.NET,新建一个控制台应用程序,名称填写为“SimpleStructTest1”,位置设置为“c:CSharpSampleschp4”。2、在代码设计窗口中编辑Class1.cs。其中的代码编写如下: using System; namespace SimpleStructTest1 struct SimpleStruct private int xval; public int X get return xval;set if (value 100)xval = value;public void DisplayX() Console.WriteLine(The stored value is: 0, xval);class TestClass public static void Main() SimpleStruct ss = new SimpleStruct(); ss.X = 5; ss.DisplayX();3、按Ctrl + F5编译并运行该程序,效果如图4-4所示。 将 Point 定义为结构而不是类在运行时可以节省很多内存空间。下面的程序创建并初始化一个 100 点的数组。对于作为类实现的 Point,出现了 101 个实例对象,因为数组需要一个,它的 100 个元素每个都需要一个。程序清单:using System;namespace ConsoleApplication1 class Point public int x, y;public Point(int x, int y) this.x = x;this.y = y;class Test static void Main() Point points = new Point100;for (int i = 0; i 100; i+)pointsi = new Point(i, i*i); 如果将 Point 改为作为结构实现,如:struct Pointpublic int x, y;public Point(int x, int y) this.x = x;this.y = y;则只出现一个实例对象(用于数组的对象)。Point 实例在数组中内联分配。此优化可能会被误用。使用结构而不是类还会使应用程序运行得更慢或占用更多的内存,因为将结构实例作为值参数传递会导致创建结构的副本。案例:当向方法传递结构时,将传递该结构的副本,而传递类实例时,将传递一个引用。目标: 掌握传递结构的方法步骤:1、启动VS.NET,新建一个控制台应用程序,名称填写为“ClasstakerTest”,位置设置为“c:CSharpSampleschp4”。2、在代码设计窗口中编辑Class1.cs。其中的代码编写如下:using System;namespace ClasstakerTest class TheClass public int x;struct TheStruct public int x;class TestClass public static void structtaker(TheStruct s)s.x = 5; public static void classtaker(TheClass c) c.x = 5; public static void Main() TheStruct a = new TheStruct();TheClass b = new TheClass();a.x = 1;b.x = 1;structtaker(a);classtaker(b);Console.WriteLine(a.x = 0, a.x);Console.WriteLine(b.x = 0, b.x);3、按Ctrl + F5编译并运行该程序本示例的输出表明:当向classtaker方法传递类实例时,只更改了类字段的值。但是向 structtaker方法传递结构实例并不更改结构字段。这是因为向structtaker方法传递的是结构的副本,而向classtaker方法传递的是对类的引用。结构可以声明构造函数,但它们必须带参数。声明结构的默认(无参数)构造函数是错误的。结构成员不能有初始值设定项。总是提供默认构造函数以将结构成员初始化为它们的默认值。使用New运算符创建结构对象时,将创建该结构对象,并且调用适当的构造函数。与类不同的是,结构的实例化可以不使用New运算符。如果不使用“新建”(new),那么在初始化所有字段之前,字段将保持未赋值状态,且对象不可用。 对于结构,不像类那样存在继承。一个结构不能从另一个结构或类继承,而且不能作为一个类的基。但是,结构从基类对象继承。结构可实现接口,而且实现方式与类实现接口的方式完全相同。以下是结构实现接口的代码片段:interface IImage void Paint();struct Picture : IImage public void Paint() / painting code goes here private int x, y, z; / other struct members结构使用简单,并且有时证明很有用。 但要牢记:结构在堆栈中创建,并且您不是处理对结构的引用,而是直接处理结构。每当需要一种将经常使用的类型,而且大多数情况下该类型只是一些数据时,结构可能是最佳选择。4.4.2 接口 什么是接口 接口(interface)用来定义一种程序的协定。实现接口的类或者结构要与接口的定义严格一致。有了这个协定,就可以抛开编程语言的限制(理论上)。接口可以从多个基接口继承,而类或结构可以实现多个接口。接口可以包含方法、属性、事件和索引器。接口本身不提供它所定义的成员的实现。接口只指定实现该接口的类或接口必须提供的成员。接口好比一种模版,这种模版定义了对象必须实现的方法,其目的就是让这些方法可以作为接口实例被引用。接口不能被实例化。类可以实现多个接口并且通过这些实现的接口被索引。接口变量只能索引实现该接口的类的实例。例子:interface ImyExample string thisint index get ; set ; event EventHandler Even ; void Find(int value) ; string Point get ; set ; public delegate void EventHandler(object sender, Event e) ; 上面例子中的接口包含一个索引this、一个事件Even、一个方法Find和一个属性Point。接口可以支持多重继承。就像在下例中,接口“IcomboBox”同时从“ItextBox”和“IlistBox”继承。interface IControl void Paint( ) ; interface ITextBox: IControl void SetText(string text) ; interface IListBox: IControl void SetItems(string items) ; interface IComboBox: ITextBox, IListBox 类和结构可以多重实例化接口。就像在下例中,类“EditBox”继承了类“Control”,同时从“IdataBound”和“Icontrol”继承。interface IDataBound void Bind(Binder b) ; public class EditBox: Control, IControl, IDataBound public void Paint( ) ; public void Bind(Binder b) . 在上面的代码中,“Paint”方法从“Icontrol”接口而来;“Bind”方法从“IdataBound”接口而来,都以“public”的身份在“EditBox”类中实现。说明:1、C#中的接口是独立于类来定义的。这与 C+模型是对立的,在 C+中接口实际上就是抽象基类。2、接口和类都可以继承多个接口。3、而类可以继承一个基类,接口根本不能继承类。这种模型避免了 C+的多继承问题,C+中不同基类中的实现可能出现冲突。因此也不再需要诸如虚拟继承和显式作用域这类复杂机制。C#的简化接口模型有助于加快应用程序的开发。4、一个接口定义一个只有抽象成员的引用类型。C#中一个接口实际所做的,仅仅只存在着方法标志,但根本就没有执行代码。这就暗示了不能实例化一个接口,只能实例化一个派生自该接口的对象。5、接口可以定义方法、属性和索引。所以,对比一个类,接口的特殊性是:当定义一个类时,可以派生自多重接口,而你只能可以从仅有的一个类派生。接口与组件 接口描述了组件对外提供的服务。在组件和组件之间、组件和客户之间都通过接口进行交互。因此组件一旦发布,它只能通过预先定义的接口来提供合理的、一致的服务。这种接口定义之间的稳定性使客户应用开发者能够构造出坚固的应用。一个组件可以实现多个组件接口,而一个特定的组件接口也可以被多个组件来实现。 组件接口必须是能够自我描述的。这意味着组件接口应该不依赖于具体的实现,将实现和接口分离彻底消除了接口的使用者和接口的实现者之间的耦合关系,增强了信息的封装程度。同时这也要求组件接口必须使用一种与组件实现无关的语言。目前组件接口的描述标准是IDL语言。由于接口是组件之间的协议,因此组件的接口一旦被发布,组件生产者就应该尽可能地保持接口不变,任何对接口语法或语义上的改变,都有可能造成现有组件与客户之间的联系遭到破坏。 每个组件都是自主的,有其独特的功能,只能通过接口与外界通信。当一个组件需要提供新的服务时,可以通过增加新的接口来实现。不会影响原接口已存在的客户。而新的客户可以重新选择新的接口来获得服务。组件化程序设计 组件化程序设计方法继承并发展了面向对象的程序设计方法。它把对象技术应用于系统设计,对面向对象的程序设计的实现过程作了进一步的抽象。我们可以把组件化程序设计方法用作构造系统的体系结构层次的方法,并且可以使用面向对象的方法很方便地实现组件。 组件化程序设计强调真正的软件可重用性和高度的互操作性。它侧重于组件的产生和装配,这两方面一起构成了组件化程序设计的核心。 组件的产生过程不仅仅是应用系统的需求,组件市场本身也推动了组件的发展,促进了软件厂商的交流与合作。组件的装配使得软件产品可以采用类似于搭积木的方法快速地建立起来,不仅可以缩短软件产品的开发周期,同时也提高了系统的稳定性和可靠性。组件程序设计的方法有以下几个方面的特点:1、编程语言和开发环境的独立性;2、组件位置的透明性;3、组件的进程透明性;4、可扩充性;5、可重用性;6、具有强有力的基础设施;7、系统一级的公共服务。 C#语言由于其许多优点,十分适用于组件编程。但这并不是说C#是一门组件编程语言,也不是说C#提供了组件编程的工具。我们已经多次指出,组件应该具有与编程语言无关的特性。请读者记住这一点:组件模型是一种规范,不管采用何种程序语言设计组件,都必须遵守这一规范。比如组装计算机的例子,只要各个厂商为我们提供的配件规格、接口符合统一的标准,这些配件组合起来就能协同工作,组件编程也是一样。我们只是说,利用C#语言进行组件编程将会给我们带来更大的方便。知道了什么是接口,接下来就是怎样定义接口。 定义接口 从技术上讲,接口是一组包含了函数型方法的数据结构。通过这组数据结构,客户代码可以调用组件对象的功能。定义接口的一般形式为: 接口修饰符 interface 接口名 : 基类接口名 /接口的成员; 其中接口的修饰符可以是new、public、protected、internal和private。New修饰符是在嵌套接口中唯一允许存在的修饰符,它说明用相同的名称隐藏一个继承的成员。Public、proteced、internal和pricate修饰符控制接口的访问能力。 接口这个概念在C#和Java中非常相似。接口的关键词是interface,一个接口可以扩展一个或者多个其他接口。按照惯例,接口的名字以大写字母“I”开头。下面的代码是C#接口的一个例子,它与Java中的接口完全一样:interface IShape void Draw ( ) ;如果你从两个或者两个以上的接口派生,父接口的名字列表用逗号分隔,如下面的代码所示:interface INewInterface: IParent1, IParent2 然而,与Java不同,C#中的接口不能包含域(Field)。另外还要注意,在C#中,接口内的所有方法默认都是公用方法。在Java中,方法定义可以带有public修饰符(即使这并非必要),但在C#中,显式为接口的方法指定public修饰符是非法的。例如,下面的C#接口将产生一个编译错误。基接口 一个接口可以从零或多个接口继承,那些被称为这个接口的显式基接口。当一个接口有比零多的显式基接口时,那么在接口的定义中的形式为,接口标识符后面跟着由一个冒号“:”和一个用逗号“,”分开的基接口标识符列表。接口基接口类型列表说明:1、一个接口的显式基接口必须至少同接口本身一样可访问。例如,在一个公共接口的基接口中指定一个私有或内部的接口是错误的。2、一个接口直接或间接地从它自己继承是错误的。3、接口的基接口都是显式基接口,并且是它们的基接口。换句话说,基接口的集合完全由显式基接口和它们的显式基接口等等组成。4、一个接口继承它的基接口的所有成员。5、一个实现了接口的类或结构也隐含地实现了所有接口的基接口。定义接口主要是定义接口成员,我们在下面介绍接口成员。 定义接口成员 接口可以包含一个和多个成员,这些成员可以是方法、属性、索引指示器和事件,但不能是常量、域、操作符、构造函数或析构函数,而且不能包含任何静态成员。接口定义创建新的定义空间,并且接口定义直 接包含的接口成员定义将新成员引入该定义空间。说明:1、接口的成员是从基接口继承的成员和由接口本身定义的成员。2、接口定义可以定义零个或多个成员。接口的成员必须是方法、属性、事件或索引器。接口不能包含常数、字段、运算符、实例构造函数、析构函数或类型,也不能包含任何种类的静态成员。3、定义一个接口,该接口对于每种可能种类的成员都包含一个:方法、属性、事件和索引器。4、接口成员默认访问方式是public。接口成员定义不能包含任何修饰符,比如成员定义前不能加abstract,public,protected,internal,private,virtual,override 或static 修饰符。5、接口的成员之间不能相互同名。继承而来的成员不用再定义,但接口可以定义与继承而来的成员同名的成员,这时我们说接口成员覆盖了继承而来的成员,这不会导致错误,但编译器会给出一个警告。关闭警告提示的方式是在成员定义前加上一个new关键字。但如果没有覆盖父接口中的成员,使用new 关键字会导致编译器发出警告。6、方法的名称必须与同一接口中定义的所有属性和事件的名称不同。此外,方法的签名必须与同一接口中定义的所有其他方法的签名不同。7、属性或事件的名称必须与同一接口中定义的所有其他成员的名称不同。8、一个索引器的签名必须区别于在同一接口中定义的其他所有索引器的签名。9、接口方法声明中的属性(attributes), 返回类型(return-type), 标识符(identifier), 和形式参数列表(formal-parameter-lis)与一个类的方法声明中的那些有相同的意义。一个接口方法声明不允许指定一个方法主体,而声明通常用一个分号结束。10、接口属性声明的访问符与类属性声明的访问符相对应,除了访问符主体通常必须用分号。因此,无论属性是读写、只读或只写,访问符都完全确定。11、接口索引声明中的属性(attributes), 类型(type), 和形式参数列表 (formal-parameter-list)与类的索引声明的那些有相同的意义。 接口成员的全权名 使用接口成员也可采用全权名(fully qualified name)。接口的全权名称是这样构成的。接口名加小圆点“.” 再跟成员名比如对于下面两个接口:interface IControl void Paint( ) ;interface ITextBox: IControl void GetText(string text) ;其中Paint 的全权名是IControl.Paint,GetText的全权名是ITextBox. GetText。当然,全权名中的成员名称必须是在接口中已经定义过的,比如使用ITextBox.Paint.就是不合理的。如果接口是名字空间的成员,全权名还必须包含名字空间的名称。namespace System public interface IdataTable object Clone( ) ; 那么Clone方法的全权名是System. IDataTable.Clone。 定义好了接口,接下来就是怎样访问接口,下面对访问接口进行介绍。访问接口对接口成员的访问 对接口方法的调用和采用索引指示器访问的规则与类中的情况也是相同的。如果底层成员的命名与继承而来的高层成员一致,那么底层成员将覆盖同名的高层成员。但由于接口支持多继承,在多继承中,如果两个父接口含有同名的成员,这就产生了二义性(这也正是C#中取消了类的多继承机制的原因之一),这时需要进行显式的定义。 类对接口的实现 前面我们已经说过,接口定义不包括方法的实现部分。接口可以通过类或结构来实现。我们主要讲述通过类来实现接口。用类来实现接口时,接口的名称必须包含在类定义中的基类列表中。实现接口1、显式实现接口成员 为了实现接口,类可以定义显式接口成员执行体(Explicit interface member implementations)。显式接口成员执行体可以是一个方法、一个属性、一个事件或者是一个索引指示器的定义,定义与该成员对应的全权名应保持一致。using System ;interface ICloneable object Clone( ) ; interface IComparable int CompareTo(object other) ;class ListEntry: ICloneable, IComparable object ICloneable.Clone( ) int IComparable.CompareTo(object other) 上面的代码中ICloneable.Clone 和IComparable.CompareTo 就是显式接口成员执行体。 说明:(1)、不能在方法调用、属性访问以及索引指示器访问中通过全权名访问显式接口成员执行体。事实上,显式接口成员执行体只能通过接口的实例,仅仅引用接口的成员名称来访问。(2)、显式接口成员执行体不能使用任何访问限制符,也不能加上abstract, virtual, override或static 修饰符。(3)、显式接口成员执行体和其他成员有着不同的访问方式。因为不能在方法调用、属性访问以及索引指示器访问中通过全权名访问,显式接口成员执行体在某种意义上是私有的。但它们又可以通过接口的实例访问,也具有一定的公有性质。(4)、只有类在定义时,把接口名写在了基类列表中,而且类中定义的全权名、类型和返回类型都与显式接口成员执行体完全一致时,显式接口成员执行体才是有效的,例如: class Shape: ICloneable object ICloneable.Clone( ) int IComparable.CompareTo(object other) 使用显式接口成员执行体通常有两个目的:(1)、因为显式接口成员执行体不能通过类的实例进行访问,这就可以从公有接口中把接口的实现部分单独分离开。如果一个类只在内部使用该接口,而类的使用者不会直接使用到该接口,这种显式接口成员执行体就可以起到作用。(2)、显式接口成员执行体避免了接口成员之间因为同名而发生混淆。如果一个类希望对名称和返回类型相同的接口成员采用不同的实现方式,这就必须要使用到显式接口成员执行体。如果没有显式接口成员执行体,那么对于名称和返回类型不同的接口成员,类也无法进行实现。2、继承接口实现 接口具有不变性,但这并不意味着接口不再发展。类似于类的继承性,接口也可以继承和发展。 注意:接口继承和类继承不同,首先,类继承不仅是说明继承,而且也是实现继承;而接口继承只是说明继承。也就是说,派生类可以继承基类的方法实现,而派生的接口只继承了父接口的成员方法说明,而没有继承父接口的实现,其次,C#中类继承只允许单继承,但是接口继承允许多继承,一个子接口可以有多个父接口。接口可以从零或多个接口中继承。从多个接口中继承时,用“:”后跟被继承的接口名字,多个接口名之间用“,”分割。被继承的接口应该是可以访问得到的,比如从private 类型或internal 类型的接口中继承就是不允许的。接口不允许直接或间接地从自身继承。和类的继承相似,接口的继承也形成接口之间的层次结构。3、重新实现接口 我们已经介绍过,派生类可以对基类中已经定义的成员方法进行重载。类似的概念引入到类对接口的实现中来,叫做接口的重实现(re-implementation)。继承了接口实现的类可以对接口进行重实现。这个接口要求是在类定义的基类列表中出现过的。对接口的重实现也必须严格地遵守首次实现接口的规则,派生的接口映射不会对为接口的重实现所建立的接口映射产生任何影响。下面的代码给出了接口重实现的例子:interface IControl void Paint( ) ; class Control: Icontrol void IControl.Paint( ) class MyControl: Control, Icontrol public void Paint( ) 实际上就是:Control把IControl.Paint映射到了Control.IControl.Paint上,但这并不影响在MyControl中的重实现。在MyControl中的重实现中,IControl.Paint被映射到MyControl.Paint 之上。 在接口的重实现时,继承而来的公有成员定义和继承而来的显式接口成员的定义参与到接口映射的过程。using System ;interface IMethods void F( ) ; void G( ) ; void H( ) ; void I( ) ;class Base: IMethods void IMethods.F( ) void IMethods.G( ) public void H( ) public void I( ) class Derived: Base, Imethods public void F( ) void IMethods.H( ) 这里,接口IMethods在Derived中的实现把接口方法映射到了Derived.F,Base.IMethods.G, Derived.IMethods.H, 还有Base.I。前面我们说过,类在实现一个接口时,同时隐式地实现了该接口的所有父接口。同样,类在重实现一个接口时同时,隐式地重实现了该接口的所有父接口。using System ;interface IBase void F( ) ; interface IDerived: IBase void G( ) ; class C: IDerived void IBase.F( ) /对F 进行实现的代码 void IDerived.G( ) /对G 进行实现的代码 class D: C, IDerived public void F( ) /对F 进行实现的代码 public void G( ) /对G 进行实现的代码 这里,对IDerived的重实现也同样实现了对IBase的重实现,把IBase.F 映射到了D.F。4、映射接口 类必须为在基类表中列出的所有接口的成员提供具体的实现。在类中定位接口成员的实现称之为接口映射(interface mapping )。 映射,数学上表示一一对应的函数关系。接口映射的含义也是一样,接口通过类来实现,那么对于在接口中定义的每一个成员,都应该对应着类的一个成员来为它提供具体的实现。 类的成员及其所映射的接口成员之间必须满足下列条件:(1)、如果A和B都是成员方法,那么A和B的名称、类型、形参表(包括参数个数和每一个参数的类型)都应该是一致的。(2)、如果A和B都是属性,那么A和B的名称、类型应当一致,而且A和B的访问器也是类似的。但如果A不是显式接口成员执行体,A允许增加自己的访问器。(3)、如果A和B都是时间那么A和B的名称、类型应当一致。(4)、如果A和B都是索引指示器,那么A和B的类型、形参表(包括参数个数和每一个参数的类型)应当一致。而且A和B的访问器也是类似的。但如果A不是显式接口成员执行体,A允许增加自己的访问器。 那么,对于一个接口成员,怎样确定由哪一个类的成员来实现呢?即一个接口成员映射的是哪一个类的成员? 在这里,我们叙述一下接口映射的过程。假设类C实现了一个接口IInterface,Member是接口IInterface中的一个成员,在定位由谁来实现接口成员Member,即Member的映射过程是这样的: (1)、如果C中存在着一个显式接口成员执行体,该执行体与接口IInterface 及其成员Member相对应,则由它来实现Member 成员。(2)、如果条件(1)不满足,且C中存在着一个非静态的公有成员,该成员与接口成员Member相对应,则由它来实现Member 成员。(3)、如果上述条件仍不满足,则在类C定义的基类列表中寻找一个C 的基类D,用D来代替C。(4)、重复步骤1- 3,遍历C的所有直接基类和非直接基类,直到找到一个满足条件的类的成员。(5)、如果仍然没有找到,则报告错误。 下面是一个调用基类方法来实现接口成员的例子。类Class2 实现了接口Interface1,类Class2 的基类Class1 的成员也参与了接口的映射,也就是说类Class2 在对接口Interface1进行实现时,使用了类Class1提供的成员方法F来实现接口Interface1的成员方法F: interface Interface1 void F( ) ; class Class1 public void F( ) public void G( ) class Class2: Class1, Interface1 new public void G( ) 注意:接口的成员包括它自己定义的成员,而且包括该接口所有父接口定义的成员。在接口映射时,不仅要对接口定义体中显式定义的所有成员进行映射,而且要对隐式地从父接口那里继承来的所有接口成员进行映射。 在进行接口映射时,还要注意下面两点:1、在决定由类中的哪个成员来实现接口成员时,类中显式说明的接口成员比其它成员优先实现。2、使用Private、protected和static修饰符的成员不能参与实现接口映射。我们对C#的接口有了较全面的认识,基本掌握了怎样应用C#的接口编程,但事实上,C#的不仅仅应用于.NET平台,它同样支持以前的COM,可以实现COM类到.NET类的转换,如C#调用API。我们将在下面的章节中讲解。 接口转换 C#中不仅支持.Net 平台,而且支持COM平台。为了支持 COM和.Net,C# 包含一种称为属性的独特语言特性。一个属性实际上就是一个 C# 类,它通过修饰源代码来提供元信息。属性使 C# 能够支持特定的技术,如 COM 和 .Net,而不会干扰语言规范本身。C# 提供将COM接口转换为 C#接口的属性类。另一些属性类将 COM类转换为C# 类。执行这些转换不需要任何IDL或类工厂。 现在部署的任何COM 组件都可以在接口转换中使用。通常情况下,所需的调整是完全自动进行的。 特别是,可以使用运行时可调用包装 (RCW) 从 .NET 框架访问 COM 组件。此包装将 COM 组件提供的 COM 接口转换为与 .NET 框架兼容的接口。对于 OLE 自动化接口,RCW 可以从类型库中自动生成;对于非 OLE 自动化接口,开发人员可以编写自定义 RCW,手动将 COM 接口提供的类型映射为与 .NET 框架兼容的类型。 使用ComImport引用COM组件 COM Interop 提供对现有 COM 组件的访问,而不需要修改原始组件。使用ComImport引用COM组件常包括下面 几个方面的问题:1、创建 COM 对象。2、确定 COM 接口是否由对象实现。3、调用 COM 接口上的方法。4、实现可由 COM 客户端调用的对象和接口。(1)创建 COM 类包装 要使 C# 代码引用COM 对象和接口,需要在 C# 中包含 COM 接口的定义。完成此操作的最简单方法是使用TlbImp.exe(类型库导入程序),它是一个包括在 .NET 框架 SDK 中的命令行工具。TlbImp将COM类型库转换为 .NET 框架元数据,从而有效地创建一个可以从任何托管语言调用的托管包装。用TlbImp创建的 .NET 框架元数据可以通过/R编译器选项包括在 C# 内部版本中。如果使用Visual Studio开发环境,则只需添加对COM 类型库的引用,将为您自动完成此转换。TlbImp 执行下列转换:1)、COM coclass 转换为具有无参数构造函数的 C# 类。2)、COM 结构转换为具有公共字段的 C# 结构。检查 TlbImp 输出的一种很好的方法是运行 .NET 框架 SDK 命令行工具 Ildasm.exe(Microsoft 中间语言反汇编程序)来查看转换结果。 虽然 TlbImp 是将 COM 定义转换为 C# 的首选方法,但也不是任何时候都可以使用它(例如,在没有 COM 定义的类型库时或者 TlbImp 无法处理类型库中的定义时,就不能使用该方法)。在这些情况下,另一种方法是使用 C# 属性在 C# 源代码中手动定义 COM 定义。创建 C# 源映射后,只需编译 C# 源代码就可产生托管包装。执行 COM 映射需要理解的主要属性包括:1)、ComImport:它将类标记为在外部实现的 COM 类。2)、Guid:它用于为类或接口指定通用唯一标识符 (UUID)。3)、InterfaceType,它指定接口是从 IUnknown 还是从 IDispatch 派生。4)、PreserveSig,它指定是否应将本机返回值从 HRESULT 转换为 .NET 框架异常。(2)声明 COM coclass COM coclass 在 C# 中表示为类。这些类必须具有与其关联的 ComImport 属性。下列限制适用于这些类:1)、类不能从任何其他类继承。2)、类不能实现任何接口。3)、类还必须具有为其设置全局唯一标识符 (GUID) 的 Guid 属性。以下示例在 C# 中声明一个 coclass:/ 声明一个COM类 FilgraphManagerComImport, Guid(E436EBB3-524F-11CE-9F53-0020AF0BA770)class FilgraphManager C# 编译器将添加一个无参数构造函数,可以调用此构造函数来创建 COM coclass 的实例。(3)创建 COM 对象 COM coclass 在 C# 中表示为具有无参数构造函数的类。使用 new 运算符创建该类的实例等效于在 C# 中调用 CoCreateInstance。使用以上定义的类,就可以很容易地实例化此类: class MainClass public static void Main() FilgraphManager filg = new FilgraphManager(); (4)声明 COM 接口COM 接口在 C# 中表示为具有 ComImport 和 Guid 属性的接口。它不能在其基接口列表中包含任何接口,而且必须按照方法在 COM 接口中出现的顺序声明接口成员函数。在 C# 中声明的COM接口必须包含其基接口的所有成员的声明,IUnknown和IDispatch的成员除外(.NET 框架将自动添加这些成员)。从 IDispatch 派生的 COM 接口必须用 InterfaceType 属性予以标记。从 C# 代码调用 COM 接口方法时,公共语言运行库必须封送与 COM 对象之间传递的参数和返回值。对于每个 .NET 框架类型均有一个默认类型,公共语言运行库将使用此默认类型在 COM 调用间进行封送处理时封送。例如,C# 字符串值的默认封送处理是封送到本机类型 LPTSTR(指向 TCHAR 字符缓冲区的指针)。可以在 COM 接口的 C# 声明中使用 MarshalAs 属性重写默认封送处理。在 COM 中,返回成功或失败的常用方法是返回一个 HRESULT,并在 MIDL 中有一个标记为retval、用于方法的实际返回值的 out 参数。在 C#(和 .NET 框架)中,指示已经发生错误的标准方法是引发异常。默认情况下,.NET 框架为由其调用的 COM 接口方法在两种异常处理类型之间提供自动映射。返回值更改为标记为 retval 的参数的签名(如果方法没有标记为 retval 的参数,则为 void)。标记为 retval 的参数从方法的参数列表中剥离。任何非成功返回值都将导致引发 System.COMException 异常。此示例显示用 MIDL 声明的 COM 接口以及用 C# 声明的同一接口(注意这些方法使用 COM 错误处理方法)。在 .NET 框架程序中通过DllImport使用 Win32 API.NET 框架程序可以通过静态 DLL 入口点的方式来访问本机代码库。DllImport 属性用于指定包含外部方法的实现的dll 位置。 DllImport 属性定义如下:namespace System.Runtime.InteropServices AttributeUsage(AttributeTargets.Method) public class DllImportAttribute: System.Attribute public DllImportAttribute(string dllName) . public CallingConvention CallingConvention; public CharSet CharSet; public string EntryPoint; public bool ExactSpelling; public bool PreserveSig; public bool SetLastError; public string Value get . 说明:1、DllImport只能放置在方法声明上。2、DllImport具有单个定位参数:指定包含被导入方法的 dll 名称的 dllName 参数。3、DllImport具有五个命名参数:a、CallingConvention参数指示入口点的调用约定。如果未指定CallingConvention,则使用默认值CallingConvention.Winapi。b、CharSet参数指示用在入口点中的字符集。如果未指定CharSet,则使用默认值CharSet.Auto。c、EntryPoint 参数给出dll中入口点的名称。如果未指定EntryPoint,则使用方法本身的名称。d、ExactSpelling 参数指示 EntryPoint 是否必须与指示的入口点的拼写完全匹配。如果未指定 ExactSpelling,则使用默认值 false。f、SetLastError 参数指示方法是否保留 Win32上一错误。如果未指定 SetLastError,则使用默认值 false。4、它是一次性属性类。5、此外,用 DllImport 属性修饰的方法必须具有 extern 修饰符。面向对象的编程语言几乎都用到了抽象类这一概念,抽象类为实现抽象事物提供了更大的灵活性。C#也不例外, C#通过覆盖虚接口的技术深化了抽象类的应用。覆盖虚接口 有时候我们需要表达一种抽象的东西,它是一些东西的概括,但我们又不能真正的看到它成为一个实体在我们眼前出现,为此面向对象的编程语言便有了抽象类的概念。C#作为一个面向对象的语言,必然也会引入抽象类这一概念。接口和抽象类使您可以创建组件交互的定义。通过接口,可以指定组件必须实现的方法,但不实际指定如何实现方法。抽象类使您可以创建行为的定义,同时提供用于继承类的一些公共实现。对于在组件中实现多态行为,接口和抽象类都是很有用的工具。 一个抽象类必须为类的基本类列表中列出的接口的所有成员提供实现程序。但是,一个抽象类被允许把接口方法映射到抽象方法中。例如:interface IMethods void F(); void G();abstract class C: Imethods public abstract void F(); public abstract void G(); 这里, IMethods 的实现函数把F和G映射到抽象方法中,它们必须在从C派生的非抽象类中被覆盖。 注意显式接口成员实现函数不能是抽象的,但是显式接口成员实现函数当然可以调用抽象方法。例如:interface Imethods void F(); void G();abstract class C: Imethods void IMethods.F() FF(); void IMethods.G() GG(); protected abstract void FF(); protected abstract void GG(); 这里,从C派生的非抽象类要覆盖FF和GG, 因此提供了IMethods的实际实现程序。 4.5 集合与索引器 4.5.1 集合 集合基本上是由一群相同类型的对象所组成的。利用集合,可以使用相同的语法,一次处理多个对象。.NET对于集合的支持,集中于System.Collection命名空间里的一组集合接口以及实现这些接口的派生类;例如,Stack类实现ICollection接口并且提供后进先出(last-in first-out)的数据结构集合对象,Hashtable类则实现Idictionary接口,为一应用散列算法,提供高效率搜索索引键值得字典型键值数据集合;这些不同的类与接口,定义了实现集合所需的功能。要了解.NET对于集合所提供的支持,首先必须从IEnumerable以及IEnumerator这两个接口开始进行说明,其提供了从集合中,存取元素对象的基础功能,定义列举集合内所含元素的相关方法。所有的集合类,均会继承IEnumerable这个接口,这样做让集合对象能够支持foreach语法,支持在集合中利用循环,一一浏览列举其中元素的功能;Ienumerator由IEnumerable接口所定义的GetEnumerator方法所取得,其定义了存取集合元素所需的相关方法。 ICollection接口是所有集合的基类,继承IEnumerable接口,拥有最基础的集合列举功能,另外同时定义了各种集合所需的共同方法以及成员属性,为整个集合架构里的核心接口,其他的集合必须继承这个接口,实现其定义的相关成员,提供特定类型集合所需的功能。 IList接口以ICollection接口为基础,除了继承ICollection接口所提供的方法,并且根据自身需求另外定义其专属得方法成员,主要提供以索引存取集合元素的操作支持。 IDictionary接口同样继承了ICollection接口,支持以键/值对的方式对集合的元素作存取,继承这个接口的集合对象,将会存储成对得键/值提供作元素存取。 IEnumerator接口为提供键/值对的枚举集合接口,一个支持以键/值对方式存储的集合,其GetEnumerator方法将会返回这个类型的枚举类型对象。 IComparer接口定义了比较两个集合中对象的比较方法。ICollection接口是.NET Framework提供的标准集合接口 所有的集合都会实现这个接口,它定义了所有集合必须具备的方法与属性成员,其中包含一个方法CopyTo,定义的形式如下: void CopyTo( Array array, int index ); 注意:array作为从 ICollection 复制的元素的目标位置的一维Array。Array必须具有从零开始的索引。 Index:array中的从零开始的索引,从此处开始复制。 ICollection接口另外提供了3个属性,其中比较重要且经常被用到的是Count属性,其定义如下: Count get ; 这个属性成员,用以返回集合中所存在的元素个数值。 IList接口与实现类 表示可按照索引单独访问的一组对象。IList是ICollection接口的子代,并且是所有列表的抽象基类。IList实现有三种类别:只读、固定大小、可变大小。无法修改只读IList。固定大小的IList不允许添加或移除元素,但允许修改现有元素。可变大小的IList允许添加、移除和修改元素。其格式如下: public interface IList : ICollection, IEnumerable 其主要的方法成员:(1)int Add(object value); 将一项添加到 IList。(2)void Clear();从 IList 中移除所有项。(3)bool Contains(object value);确定 IList 是否包含特定的值。(4)int IndexOf(object value); 确定 IList 中特定项的索引。(5)void Insert(int index, object value); 在 IList 的指定位置处插入一项。(6)void Remove(object value); 从 IList 移除特定对象的第一个匹配项。(7)void RemoveAt(int index); 在指定的索引处移除 IList 项。IDictionary 类是键/值对的集合的基接口 每个元素是一个存储在DictionaryEntry对象中的键/值对。 每个关联必须具有非空引用(Visual Basic 中为Nothing)的唯一键,但关联的值可以为任何对象引用,包括空引用 (Nothing)。IDictionary接口允许对所包含的键和值进行枚举,但这并不意味着任何特定的排序顺序。 IDictionary实现有三种类别:只读、固定大小、可变大小。无法修改只读 IDictionary。固定大小的IDictionary不允许添加或移除元素,但允许修改现有元素。可变大小的IDictionary允许添加、移除和修改元素。 C# 语言中的foreach语句(在Visual Basic中为foreach)需要集合中每个元素的类型。由于IDictionary的每个元素都是一个键/值对,因此元素类型既不是键的类型,也不是值的类型。而是DictionaryEntry类型。例如:foreach (DictionaryEntry myDE in myHashtable) .VisualBasic Dim myDE As DictionaryEntryFor Each myDE In myHashtable .Next myDE foreach语句是对枚举数的包装,它只允许从集合中读取,不允许写入集合。IEnumerator接口 IEnumerator是所有枚举数的基接口。 枚举数只允许读取集合中的数据。枚举数无法用于修改基础集合。 最初,枚举数被定位于集合中第一个元素的前面。Reset也将枚举数返回到此位置。在此位置,调用Current会引发异常。因此,在读取Current的值之前,必须调用MoveNext将枚举数提前到集合的第一个元素。 在调用MoveNext或Reset之前,Current返回同一对象。MoveNext将Current设置为下一个元素。 在传递到集合的末尾之后,枚举数放在集合中最后一个元素后面,且调用MoveNext会返回false。如果最后一次调用MoveNext返回false,则调用Current会引发异常。若要再次将Current设置为集合的第一个元素,可以调用Reset,然后再调用MoveNext。 只要集合保持不变,枚举数就将保持有效。如果对集合进行了更改(例如添加、修改或删除元素),则该枚举数将失效且不可恢复,并且下一次对MoveNext或Reset的调用将引发InvalidOperationException。如果在MoveNext和Current之间修改集合,那么即使枚举数已经无效,Current也将返回它所设置成的元素。 枚举数没有对集合的独占访问权;因此,枚举一个集合在本质上不是一个线程安全的过程。甚至在对集合进行同步处理时,其他线程仍可以修改该集合,这会导致枚举数引发异常。若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合,或者捕捉由于其他线程进行的更改而引发的异常。 IEnumerator接口定义了一个只读属性,定义如下: object Current get; 这个属性取得目前列举的元素,你无法利用这个属性,修改集合中的对象元素,IEnumerator接口另外定义了一个浏览集合对象的方法MoveNext,任何实现这个接口的类,均必须实现此方法,以提供浏览集合对象的功能,定义如下:bool MoveNext();此方法返回一个布尔数据类型的结果,当浏览至集合结尾的时候,这个值会返回flase,否则均为true。 IEnumerator接口另外定义的一个方法为Reset,这个方法将枚举数设置为其初始位置,该位置位于集合中第一个元素之前。IComparer接口 公开一种比较两个对象的方法。 此接口与 Array.Sort 和 Array.BinarySearch 方法一起使用。它提供一种自定义集合排序顺序的方法。 此接口的默认实现为 Comparer 类。 4.5.2 索引器 索引器可以像数组那样对对象进行索引访问。在C和C+中,没有索引器的概念,它是在C#首次提出的。 索引器(indexer)使得可以像数组那样对对象使用下标。已为我们提供了通过索引方式方便地访问类的数据信息的方法。 1、声明 还是让我们先来看一下索引器的声明格式: 修饰符 数据类型 this int index 访问函数体代码 索引器可以使用的修饰符有:new、public、protected、internal、private、virtual、sealed、override和abstract。一对大括号“”之间是索引器的访问声明,使用get关键字和set关键字定义了对被索引的元素的读写权限。在许多情况下,某些数据信息应该是属于类或类的实例所私有的,需要限制对这些信息的访问。而我们有时又不希望这类数据对外界完全封闭。和属性一样,索引器为我们提供了控制访问权限的另一种办法。案例:利用索引器访问一个给定的学生数组,输出学生姓名目标:掌握索引器的基本使用方法步骤:1、启动VS.NET,新建一个控制台应用程序,名称填写为“MatrixMultiplyTest”,位置设置为“c:CSharpSampleschp4”。2、在代码设计窗口中编辑Class1.cs。其中代码编写如下:using System;namespace IndexStudTest class IndexStud private string sname;public IndexStud()/构造函数 sname=new stringSmith,Rose,Mary,Robot,Hamlat;/定义类IndexStud的索引器,用于访问存储在sname数组中学生姓名信息public string thisint index getreturn snameindex;setsnameindex=value;public class App static void Main(string args)IndexStud stud=new IndexStud();for(int x=0;x5;x+)/输出存储在数组中的对象的值Console.Write(0,10:c,studx);/调用索引器的get函数,将x值传送给indexConsole.WriteLine();3、按Ctrl + F5编译并运行该程序,效果如图4-6所示。4.6 异常处理 4.6.1 异常类在C# 里,异常处理就是C# 为处理错误情况提供的一种机制。它为每种错误情况提供了定制的处理方式,并且把标识错误的代码与处理错误的代码分离开来。对.NET类来说,一般的异常类System.Exception派生于System.Object。还有许多定义好的异常类(如:System.SystemException、System.ApplicationException等),他们又派生于System.Exception类。其中System.ApplicationException类是第三方定义的异常类,如果我们要自定义异常类,那么就应派生于它。 在代码中对异常进行处理,一般要使用三个代码块:Try 块的代码是程序中可能出现错误的操作部分。Catch 块的代码是用来处理各种错误的部分(可以有多个)。必须正确排列捕获异常的catch子句,范围小的Exception放在前面的catch。即如果Exception之间存在继承关系,就应把子类的Exception放在前面的catch子句中。Finally 块的代码用来清理资源或执行要在try块末尾执行的其他操作(可以省略)。且无论是否产生异常,Finally块都会执行。不管程序写得再好,异常都可能会发生,而程序也必须能够处理可能出现的错误。所以我们要站在异常一定可能会发生的角度来编写异常处理程序,应对程序有可能发生的错误建立一个良好的异常处理策略。异常产生的时候,我们想知道的是什么原因造成的错误以及错误的相关信息。我们可以根据实际情况抛出具体类型的异常,方便捕捉到异常时做出具体的处理。在编写代码过程中,可以使用系统已定义的相关异常类以及自定义的异常类来实例化并抛出我们需要的异常。如一个不可能实现的接口,我们可以抛出“System.NotSupportedExceptiion”的异常来告诉接口的调用者。 在处理异常的时候,我们应该将可处理的具体异常分别在catch 块中做出相应处理,否则程序将终止运行。针对每一种异常,以不同方式处理,避免对所有异常做出一样的处理。 并且在异常产生时,给用户一个友好的提示(普通用户对异常的具体内容是不明白的,这就需要我们给出相关的简要信息和解决方案,或则告之联系管理员等。),并在可能的情况下给用户提供可能的选择(终止,重试,忽略),让用户来决定程序的运行方向。同时,要将异常做日志记录。但不是所有异常都是必须记录的,比如一些可预料并且能让程序解决的错误我们就不需要记录它。记录异常我们可以采取如下一些方式:在文件中记录异常。便于技术人员查看所发生的异常,从而日后对程序进行改进。在数据库中记录异常。数据库支持查询,这样在后期就能够对异常进行分类查询等操作,便于查看与管理。在Eventlog中记录异常:能够远程操作,方便系统管理员监控所有计算机的异常。 除了具体的、可预料到的异常外,还有未预料的异常。像这类异常是我们不愿意看到了,但发生了也只能暂时结束程序的运行,这里如果做好了日志就能为我们解决和调试问题带来了方便。还有,要避免使用了try-catch但没有处理异常的情况,否则就相当于给异常放行(这种情况还不如根本就不去捕获它)。处理完异常,我们还应该注意在finally块中释放相关资源、还原相关设置信息等收尾工作。 在做异常处理的时候,最好能在应用程序所有的入口处(事件处理函数,主函数,线程入口)使用try-catch。 但是不要在程序构造函数入口处添加try-catch,因为此处产生异常,它自己并没有能力来处理,因为它还没有构造完毕,只能再向外层抛出异常。 在一般情况下使用异常机制来处理错误,能使整个程序的结构清晰、代码简单(标识错误的代码与处理错误代码分离),但我们也不能盲目使用异常。而且使用异常,可能会在一定程度上影响到程序的性能(C#中使用异常一般不影响性能)。对于一些简单的、能够提前避免的错误,我们还是应该在try块外面及早做出处理。如:try int x = y/z; catch / . if(z = 0) Console.WriteLine(除数不能为零); / . try int x = y/z; catch / . 4.6.2 抛出和捕获异常 使用 try 和 catch捕获异常你肯定会对一件事非常感兴趣不要提示给用户那令人讨厌的异常消息,以便你的应用程序继续执行。要这样,你必须捕获(处理)该异常。这样使用的语句是try 和 catch。try包含可能会产生异常的语句,而catch处理一个异常,如果有异常存在的话。程序清单中用try 和 catch为DivideByZeroException实现异常处理。案例:对除数为零的捕获目标:掌握使用try 和 catch捕获异常的方法步骤:1、启动VS.NET,新建一个控制台应用程序,名称填写为“ZeroOverTest”,位置设置为“c:CSharpSampleschp4”。2、在代码设计窗口中编辑Class1.cs。其中代码编写如下:using System;namespace ZeroOverTestclass ZeroOverTest static int Zero=0;static void AFunction()int j=22/Zero;/下面的语句永远不会执行Console.WriteLine(In AFunction(); public static void Main(string args) try AFunction();catch(DivideByZeroException e) Console.WriteLine(DivideByZero 0,e);3、按Ctrl + F5编译并运行该程序,效果如图4-7所示。 如果你不事先知道哪一种异常会被预期,而仍然想处于安全状态,简单地忽略异常的类型。try .catch . 但是,通过这个途径,你不能获得对异常对象的访问,而该对象含有重要的出错信息。一般化异常处理代码象这样:try .catch(System.Exception e) . 注意,你不能用ref或out修饰符传递e对象给一个方法,也不能赋给它一个不同的值。 使用try和finally清除异常 如果你更关心清除而不是错误处理,那么try和finally会获得你的使用。它不仅抑制了出错消息,而且所有包含在finally块中的代码在异常被引发后仍然会被执行。尽管程序不正常终止,但你还可以为用户获取一条消息。 在C+中的_leave语句是用来提前终止try语段中的执行代码,并立即跳转到finally语段 。C#中没有_leave语句。但是,在下面例子中的代码演示了一个你可以实现的方案。案例:实现_leave语句的跳转目标:掌握使用try和finally清除异常的基本方法步骤: 1、启动VS.NET,新建一个控制台应用程序,名称填写为“JumpTest”,位置设置为“c:CSharpSampleschp4”。2、在代码设计窗口中编辑Class1.cs。其中的代码编写如下:using System;namespace JumpTestclass JumpTestpublic static void Main()try Console.WriteLine(try);goto _leave; finally Console.WriteLine(finally);_leave:Console.WriteLine(_leave);3、按Ctrl + F5编译并运行该程序一个goto语句不能退出一个finally语段。甚至把goto语句放在try语句段中,还是会立即返回控制到finally语段。因此,goto只是离开了try语段并跳转到finally语段。直到finally中的代码完成运行后,才能到达_leave标签。 使用try-catch-finally处理所有异常应用程序最有可能的途径是合并前面两种错误处理技术捕获错误、清除并继续执行应用程序。所有你要做的是在出错处理代码中使用try 、catch和finally语句。案例:显示处理零除错误的途径目标:掌握使用try-catch-finally处理所有异常的基本方法步骤:1、启动VS.NET,新建一个控制台应用程序,名称填写为“CatchITTest”,位置设置为“c:CSharpSampleschp4”。2、在代码设计窗口中编辑Class1.cs。其中的代码编写如下:using System;namespace CatchITTestclass CatchITpublic static void Main()tryint nTheZero = 0;int nResult = 10 / nTheZero;catch(DivideByZeroException divEx)Console.WriteLine(divide by zero occurred!);catch(Exception Ex)Console.WriteLine(some other exception);finally3、按Ctrl + F5编译并运行该程序,效果如图4-9所示。 抛出异常在C#中,除了程序发生错误产生异常外,程序员还可以自己为某种目的抛出异常。使用以下两种方式都可以抛出异常: throw Exception_obj; throw new ArgumentException(Exception);案例:定义一个抛出异常目标:掌握抛出异常的基本方法步骤:1、启动VS.NET,新建一个控制台应用程序,名称填写为“MyClientTest”,位置设置为“c:CSharpSampleschp4”。2、在代码设计窗口中编辑Class1.cs。其中的代码编写如下:using System;namespace MyClientTestclass MyClientpublic static void Main(string args)try throw new DivideByZeroException(Invalid Division);catch(DivideByZeroException e) Console.WriteLine(Exception);Console
- 温馨提示:
1: 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
2: 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
3.本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

人人文库网所有资源均是用户自行上传分享,仅供网友学习交流,未经上传用户书面授权,请勿作他用。