版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
第4章C#面向对象高级编程
前面介绍了面向对象程序设计的基本概念和应用。但是面向对象还包括很多其他重要的概念。本章将深入分析面向对象编程的概念,并详细说明利用工具进行面向对象程序设计的方法。
4.1类的继承与多态
4.1.1继承1、概述现实世界中的许多实体之间不是相互孤立的,它们往往具有共同的特征,也存在内在的差别。人们可以采用层次结构来描述这些实体之间的相似之处和不同之处。为了用软件语言对现实世界中的层次结构进行模型化,面向对象的程序设计技术引入了继承的概念。一个类从另一个类派生出来时,派生类从基类那里继承特性。派生类也可以作为其它类的基类。从一个基类派生出来的多层类形成了类的层次结构。注意:C#中,派生类只能从一个类中继承。这是因为,在C++中,人们在大多数情况下不需要一个从多个类中派生的类。从多个基类中派生一个类,这往往会带来许多问题,从而抵消了这种灵活性带来的优势。C#中,派生类从它的直接基类中继承成员:方法、域、属性、事件、索引指示器。除了构造函数和析构函数,派生类隐式地继承了直接基类的所有成员。C#中,派生类从它的直接基类中继承成员:方法、域、属性、事件、索引指示器。除了构造函数和析构函数,派生类隐式地继承了直接基类的所有成员。程序清单:usingSystem;namespaceConsoleApplication1{ classVehicle//定义汽车类
{
intwheels;//公有成员:轮子个数
protectedfloatweight;//保护成员:重量
publicVehicle(){;} publicVehicle(intw,floatg) { wheels=w; weight=g;} publicvoidSpeak() {
Console.WriteLine("thewvehicleisspeaking!"); }} classCar:Vehicle//定义轿车类:从汽车类中继承
{intpassengers;//私有成员:乘客数
publicCar(intw,floatg,intp):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,见下面的代码。程序清单:usingSystem;
namespaceConsoleApplication1
{ classVehicle//定义汽车类
{ publicintwheels;//公有成员:轮子个数
protectedfloatweight;//保护成员:重量
publicVehicle(){;} publicVehicle(intw,floatg) {wheels=w;
weight=g;} publicvoidSpeak() {
Console.WriteLine("thewvehicleisspeaking!"); } } classCar:Vehicle//定义轿车类
{
intpassengers;//私有成员:乘客数
publicCar(intw,floatg,intp) { wheels=w; weight=g;passengers=p;} newpublicvoidSpeak() {
Console.WriteLine("Di-di!"); } }}
注意:如果在成员声明中加上了new关键字修饰,而该成员事实上并没有覆盖继承的成员,编译器将会给出警告。在个成员声明同时使用new和override则编译器会报告错误。3、base保留字base关键字主要是为派生类调用基类成员提供一个简写的方法。我们先看一个例子程序代码:usingSystem;namespaceConsoleApplication1{ classA { publicvoidF() { //F的具体执行代码
}
publicint
this[int
nIndex]
{ get{} set{} } classB { publicvoidG() {
intx=base[0];
base.F(); } } }}类B从类A中继承,B的方法G中调用了A的方法F和索引指示器。方法F在进行编译时等价于:publicvoidG(){
intx=(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:\CSharpSamples\chp4”。2、在代码设计窗口中编辑Class1.cs。其中的代码编写如下:
usingSystem;namespaceDifferentiateTest{classA {publicvoidF(){Console.WriteLine("A.F");} publicvirtualvoidG(){Console.WriteLine("A.G");} } classB:A {newpublicvoidF(){Console.WriteLine("B.F");} publicoverridevoidG(){Console.WriteLine("B.G");} }
classTest{ staticvoidMain() { Bb=newB(); Aa=b;
a.F();
b.F();
a.G();
b.G(); } }}3、按Ctrl+F5编译并运行该程序,效果如图5-2所示。图4-1程序运行结果例子中,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#中引入了抽象类(abstractclass)的概念。注意:C++程序员在这里最容易犯错误。C++中没有对抽象类进行直接声明的方法,而认为只要在类中定义了纯虚函数,这个类就是一个抽象类。纯虚函数的概念比较晦涩,直观上不容易为人们接受和掌握,因此C#抛弃了这一概念。抽象类使用abstract修饰符,对抽象类的使用有以下几点规定:(1)抽象类只能作为其它类的基类,它不能直接被实例化,而且对抽象类不能使用new操作符。抽象类如果含有抽象的变量或值,则它们要么是null类型,要么包含了对非抽象类的实例的引用。(2)抽象类允许包含抽象成员,虽然这不是必须的。(3)抽象类不能同时又是密封的。
如果一个非抽象类从抽象类中派生,则其必须通过重载来实现所有继承而来的抽象成员。请看下面的示例:usingSystem;namespaceConsoleApplication1{ abstractclassA { publicabstractvoidF(); } abstractclassB:A { publicvoidG(){} }classC:B
{
publicoverridevoidFD
{
//F的具体实现代码
}
}
}抽象类A提供了一个抽象方法F。类B从抽象类A中继承,并且又提供了一个方法G;因为B中并没有包含对F的实现,所以B也必须是抽象类。类C从类B中继承,类中重载了抽象方法F,并且提供了对F的具体实现,则类C允许是非抽象的。
2、抽象方法由于抽象类本身表达的是抽象的概念,因此类中的许多方法并不一定要有具体的实现,而只是留出一个接日来作为派生类重载的界面。举一个简单的例子,“图形”这个类是抽象的,它的成员方法“计算图形面积”也就没有实际的意义。面积只对“图形”的派生类比如“圆”、“一角形”这些非抽象的概念才有效,那么我们就可以把基类“图形”的成员方法“计算面积”声明为抽象的,具体的实现交给派生类通过重载来实现。一个方法声明中如果加上abstract修饰符,我们称该方法为抽象方法(abstractmethod)。如果一个方法被声明也是抽象的,那么该方法默认也是一个虚方法。事实上,抽象方法是一个新的虚方法,它不提供具体的方法实现代码。我们知道,非虚的派生类要求通过重载为继承的虚方法提供自己的实现,而抽象方法则不包含具体的实现内容,所以方法声明的执行体中只有一个分号“;”。只能在抽象类中声明抽象方法。对抽象方法,不能再使用static或virtual修饰符,而且方法不能有任何可执行代码,哪怕只是一对大括号中间加一个一个分号“{;}”都不允许出现,只需要给出方法的原型就可以了。还要注意,抽象方法在派生类中不能使用base关键字来进行访问。例如,下面的代码在编译时会发生错误:usingSystem;namespaceConsoleApplication1{classA {publicabstractvoidF(); } classB:A {publicoverridevoidF()
{
base.F();//错误,base.F是抽象方法
}
}
}我们还可以利用抽象方法来重载基类的虚方法,这时基类中虚方法的执行代码就被“拦截”了。下面的例子说明了这一点:usingSystem;namespaceConsoleApplication1{classA {publicvirtualvoidF(){
Console.WriteLine("A.F"); } } abstractclassB:A { publicabstractoverridevoidF(); } classC:B { publicoverridevoidF() {
Console.WriteLine("C.F"); } }}类A声明了一个虚方法F,派生类B使用抽象方法重载了F,这样B的派生类C就可以重载F并提供自己的实现。3、密封类想想看,如果所有的类都可以被继承,继承的滥用会带来什么后果?类的层次结构体系将变得十分庞大,类之间的关系杂乱无章,对类的理解和使用都会变得十分困难。有时候,我们并不希望自己编写的类被继承。另一些时候,有的类己经没有再被继承的必要。C#提出了一个密封类(sealedclass)的概念,帮助开发人员来解决这一问题。密封类在声明中使用sealed修饰符,这样就可以防止该类被其它类继承。如果试图将一个密封类作为其它类的基类,C#将提不出错。理所当然,密封类不能同时又是抽象类,因为抽象总是希望被继承的。在哪些场合下使用密封类呢?密封类可以阻止其它程序员在无意中继承该类,而且密封类可以起到运行时优化的效果。实际上,密封类中不可能有派生类,如果密封类实例中存在虚成员函数,该成员函数可以转化为非虚的,函数修饰符virtual不再生效。让我们看下面的例子:usingSystem;namespaceConsoleApplication1{abstractclassA {publicabstractvoidF(); } sealedclassB:A {publicoverridevoidF()
{
//F的具体实现代码
}
}
}如果我们尝试写下面的代码:classC:B{}C#会指出这个错误,告诉你B是一个密封类,不能试图从B中派生任何类。4、密封方法我们己经知道,使用密封类可以防止对类的继承。C#还提出了密封方法(sealedmethod)的概念,以防止在方法所在类的派生类中对该方法的重载。对方法可以使用sealed修饰符,这时我们称该方法是一个密封方法。不是类的每个成员方法都可以作为密封方法,密封方法必须对基类的虚方法进行重载,提供具体的实现方法。所以,在方法的声明中,sealed修饰符总是和override修饰符同时使用。请看例子代码。程序清单:usingSystem;namespaceConsoleApplication1{ classA { publicvirtualvoidF() {
Console.WriteLine("A.F");
} publicvirtualvoidG() {Console.WriteLine("A.G"); } } classB:A {sealedoverridepublicvoidF() {Console.WriteLine("B.F"); } overridepublicvoidG() {Console.WriteLine("B.G"); } } classC:B {overridepublicvoidG()
{
Console.WriteLine("C.G");
}
}
}类B对基类A中的两个虚方法均进行了重载,其中F方法使用了Sealed修饰符,成为一个密封方法。G方法不是密封方法,所以在B的派生类C中,可以重载方法G,但不能重载方法F。
4.2操作符重载
4.2.1问题的提出在面向对象的程序设计中,自己定义一个类,就等于创建了一个新类型。类的实例和变量一样,可以作为参数传递,也可以作为返回类型。在前几章中,我们介绍了系统定义的许多操作符。比如对于两个整型变量,使用算术操作符可以简便地进行算术运算:classA { publicintx; publicinty; publicintPlus() { returnx+y; } }再比如,我们希望将属于不同类的两个实例的数据内容相加:classB {publicintx; } classTest {publicintz; publicstaticvoidMain() { Aa=newA(); Bb=newB(); z=a.x+b.x; } }使用a.x+b.x这种写法不够简洁,也不够直观。更为严重的问题是,如果类的成员在声明时使用的不是public修饰符的话,这种访问就是非法的。
我们知道,在C#中,所有数据要么属于某个类,要么属于某个类的实例,充分体现了面向对象的思想。因此,为了表达上的方便,人们希望可以重新给己定义的操作符赋予新的含义,在特定的类的实例上进行新的解释。这就需要通过操作符重载来解决。4.2.2使用成员方法重载操作符
C#中,操作符重载总是在类中进行声明,并且通过调用类的成员方法来实现。操作符重载声明的格式为:typeoperatoroperator-game(formal-param-list)
C#中,下列操作符都是可以重载的:
+-!~++--truefalse
*/%&|^<<>>==!=><>=<=但也有一些操作符是不允许进行重载的,如:
=,&&,||,?:,new,typeof,sizeof,is◆一元操作符重载顾名思义,一元操作符重载时操作符只作用于一个对象,此时参数表为空,当前对象作为操作符的单操作数。下面我们举一个角色类游戏中经常遇到的例子。扮演的角色具有内力、体力、经验值、剩余体力、剩余内力五个属性,每当经验值达到一定程度时,角色便会升级,体力、内力上升,剩余体力和内力补满。“升级”我们使用重载操作符“++”来实现。案例:游戏中“升级”问题目标:掌握一元操作符重载的基本方法步骤:1、启动VS.NET,新建一个控制台应用程序,名称填写为“PlayerTest”,位置设置为“c:\CSharpSamples\chp4”。2、在代码设计窗口中编辑Class1.cs。其中的代码编写如下:usingSystem;
namespacePlayerTest{classPlayer {publicint
neili; publicint
tili; publicint
jingyan; publicint
neili_r; publicint
tili_r; publicPlayer() {neili=10;
tili=50;
jingyan=0;
neili_r=50;
tili_r=50; } publicstaticPlayeroperator++(Playerp) {
p.neili=p.neili+50;
p.tili=p.tili+100;
p.neili_r=p.neili;
p.tili_r=p.tili;
returnp;
}publicvoidShow() {Console.WriteLine("Tili:{0}",tili);
Console.WriteLine("Jingyan:{0}",jingyan); Console.WriteLine("Neili{0}",neili);
Console.WriteLine("Tili_full:{0}",tili_r);
Console.WriteLine("Neili_full:{0}",neili_r); } }classTest
{
publicstaticvoidMain()
{
Playerman=newPlayer();
man.Show();
man++;
Console.WriteLine("Nowupgrading…:");
man.Show();
}
}
}3、按Ctrl+F5编译并运行该程序,效果如图4-2所示。◆二元操作符重载大多数情况下我们使用二元操作符重载。这时参数表中有一个参数,当前对象作为该操作符的左操作数,参数作为操作符的右操作数。图4-2程序运行结果下面我们给出二元操作符重载的一个简单例子。
案例:笛卡儿坐标相加问题
目标:掌握二元操作符重载的基本方法
步骤:1、启动VS.NET,新建一个控制台应用程序,名称填写为“DKRTest”,位置设置为“c:\CSharpSamples\chp4”。2、在代码设计窗口中编辑Class1.cs。其中的代码编写如下:usingSystem;namespaceDKRTest{classDKR {publicint
x,y,z; publicDKR(int
vx,int
vy,int
vz) {
x=vx;
y=vy;
z=vz;
}publicstaticDKRoperator+(DKRd1,DKRd2) {DKRdkr=newDKR(0,0,0);
dkr.x=d1.x+d2.x;
dkr.y=d1.y+d2.y;
dkr.z=d1.z+d2.z; returndkr; } } classTest {publicstaticvoidMain() {DKRd1=newDKR(3,2,1);
DKRd2=newDKR(0,6,5);
DKRd3=d1+d2;
Console.WriteLine("The3dlocationofd3is:{0},{1},{2}",d3.x,d3.y,d3.z);
}
}
}3、按Ctrl+F5编译并运行该程序,效果如图4-3所示图4-3程序运行结果
4.3类型转换
转换是使一种类型的表达式可以被视为另一种类型。转换可以是隐式或显式,这将确定是否需要显式地强制转换。例如,从int类型到long类型的转换是隐式的,因此int类型的表达式可隐式地按long类型处理。从long类型到int类型的反向转换是显式的,因此需要显式地强制转换。
inta=123;longb=a;//从int
类型到long类型的转换
intc=(int)b;//从long类型到int
类型的反向转换4.3.1隐式类型转换
下列转换属于隐式转换:◆标识转换◆隐式数值转换◆隐式枚举转换◆隐式常数表达式转换◆用户定义的隐式转换隐式转换可以在各种情况下发生,包括函数成员调用、强制转换表达式以及赋值。1、标识转换是在同一类型(可为任何类型)内进行转换。这种转换的存在,仅仅是为了使已具有所需类型的实体可被认为是可转换的(转换为该类型)。2、隐式数值转换为:◆从sbyte到short、int、long、float、double或decimal。
◆从byte到short、ushort、int、uint、long、ulong、floatdouble或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而言,装箱类的行为可用下列声明来描述:sealedclassT_Box:System.ValueType { Tvalue; publicT_Box(Tt) { value=t; } }T类型值v的装箱过程现在包括执行表达式newT_Box(v)和将结果实例作为object类型的值返回。因此,下面的语句:inti=123;objectbox=i;在概念上相当于:inti=123;objectbox=newint_Box(i);实际上,像上面这样的T_Box和int_Box并不存在,并且装了箱的值的动态类型也不会真的属于一个类类型。相反,T类型的装了箱的值属于动态类型T,若用is运算符来检查动态类型的话,也仅能引用类型T。例如,inti=123;
objectbox=i;
if(boxisint)
{Console.Write("Boxcontainsanint");
}将在控制台上输出字符串“Boxcontainsanint”。装箱转换隐含着复制一份欲被装箱的值。这不同于从引用类型到object类型的转换,在后一种转换中,转换后的值继续引用同一实例,只是将它当作派生程度较小的object类型而已。例如,设有下列的声明:structPoint{publicintx,y;publicPoint(intx,inty){this.x=x;
this.y=y;}}则下面的语句:
Pointp=newPoint(10,10);
objectbox=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。因此,下面的语句:objectbox=123;inti=(int)box;在概念上相当于:objectbox=newint_Box(123);inti=((int_Box)box).value;为使到给定值类型的取消装箱转换在运行时取得成功,源操作数的值必须是对某个对象的引用,而该对象先前是通过将该值类型的某个值装箱而创建的。如果源操作数为null,则将引发System.NullReferenceException。如果源操作数是对不兼容对象的引用,则将引发System.InvalidCastException。4.4结构和接口
4.4.1结构结构与类有很多相似之处:结构可以实现接口,并且可以具有与类相同的成员类型。然而,结构在几个重要方面不同于类:结构为值类型而不是引用类型,并且结构不支持继承。结构的值存储在“在堆栈上”或“内联”。细心的程序员有时可以通过聪明地使用结构来增强性能。声明一个结构,它有三个成员:一个属性、一个方法和一个私有字段,创建该结构的一个实例,并将其投入使用:案例:声明一个结构目标:了解声明结构的基本方法步骤:1、启动VS.NET,新建一个控制台应用程序,名称填写为“SimpleStructTest1”,位置设置为“c:\CSharpSamples\chp4”。2、在代码设计窗口中编辑Class1.cs。其中的代码编写如下:usingSystem;namespaceSimpleStructTest1{
struct
SimpleStruct { privateint
xval; publicintX { get { returnxval; } set {if(value<100)
xval=value;} } publicvoidDisplayX() {Console.WriteLine("Thestoredvalueis:{0}",xval); } } classTestClass {publicstaticvoidMain() {SimpleStruct
ss=newSimpleStruct();
ss.X=5;
ss.DisplayX(); } }}3、按Ctrl+F5编译并运行该程序,效果如图4-4所示。
将Point定义为结构而不是类在运行时可以节省很多内存空间。下面的程序创建并初始化一个100点的数组。对于作为类实现的Point,出现了101个实例对象,因为数组需要一个,它的100个元素每个都需要一个。图4-4程序运行结果程序清单:usingSystem;namespaceConsoleApplication1{classPoint {publicintx,y; publicPoint(intx,inty) {this.x=x;
this.y=y; } } classTest {staticvoidMain() {Point[]points=newPoint[100]; for(inti=0;i<100;i++)
points[i]=newPoint(i,i*i);} }}如果将Point改为作为结构实现,如:structPoint { publicintx,y; publicPoint(intx,inty) {
this.x=x;
this.y=y; } }则只出现一个实例对象(用于数组的对象)。Point实例在数组中内联分配。此优化可能会被误用。使用结构而不是类还会使应用程序运行得更慢或占用更多的内存,因为将结构实例作为值参数传递会导致创建结构的副本。案例:当向方法传递结构时,将传递该结构的副本,而传递类实例时,将传递一个引用。目标:掌握传递结构的方法步骤:1、启动VS.NET,新建一个控制台应用程序,名称填写为“ClasstakerTest”,位置设置为“c:\CSharpSamples\chp4”。2、在代码设计窗口中编辑Class1.cs。其中的代码编写如下:usingSystem;
namespaceClasstakerTest
{classTheClass {publicintx; }
struct
TheStruct {publicintx; } classTestClass {publicstaticvoidstructtaker(TheStructs) {
s.x=5; }
publicstaticvoidclasstaker(TheClassc)
{c.x=5;} publicstaticvoidMain() {TheStructa=newTheStruct();
TheClassb=newTheClass();
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),那么在初始化所有字段之前,字段将保持未赋值状态,且对象不可用。对于结构,不像类那样存在继承。一个结构不能从另一个结构或类继承,而且不能作为一个类的基。但是,结构从基类对象继承。结构可实现接口,而且实现方式与类实现接口的方式完全相同。以下是结构实现接口的代码片段:interfaceIImage
{
voidPaint();
}
structPicture:IImage
{
publicvoidPaint()
{
//paintingcodegoeshere
}
privateintx,y,z;//otherstructmembers
}结构使用简单,并且有时证明很有用。但要牢记:结构在堆栈中创建,并且您不是处理对结构的引用,而是直接处理结构。每当需要一种将经常使用的类型,而且大多数情况下该类型只是一些数据时,结构可能是最佳选择。4.4.2接口
什么是接口
接口(interface)用来定义一种程序的协定。实现接口的类或者结构要与接口的定义严格一致。有了这个协定,就可以抛开编程语言的限制(理论上)。接口可以从多个基接口继承,而类或结构可以实现多个接口。接口可以包含方法、属性、事件和索引器。接口本身不提供它所定义的成员的实现。接口只指定实现该接口的类或接口必须提供的成员。接口好比一种模版,这种模版定义了对象必须实现的方法,其目的就是让这些方法可以作为接口实例被引用。接口不能被实例化。类可以实现多个接口并且通过这些实现的接口被索引。接口变量只能索引实现该接口的类的实例。例子:interfaceImyExample{stringthis[intindex]{get;set;}eventEventHandlerEven;voidFind(intvalue);stringPoint{get;set;}}publicdelegatevoidEventHandler(objectsender,Evente);上面例子中的接口包含一个索引this、一个事件Even、一个方法Find和一个属性Point。
接口可以支持多重继承。就像在下例中,接口“IcomboBox”同时从“ItextBox”和“IlistBox”继承。interfaceIControl
{voidPaint();}interfaceITextBox:IControl
{voidSetText(stringtext);}interfaceIListBox:IControl
{voidSetItems(string[]items);}interfaceIComboBox:ITextBox,IListBox{}类和结构可以多重实例化接口。就像在下例中,类“EditBox”继承了类“Control”,同时从“IdataBound”和“Icontrol”继承。interfaceIDataBound
{voidBind(Binderb);}publicclassEditBox:Control,IControl,IDataBound
{publicvoidPaint();publicvoidBind(Binderb){...}}在上面的代码中,“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中的接口完全一样:interfaceIShape
{
voidDraw
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 主动脉夹层应急演练工作总结(2篇)
- 星级酒店市场营销部年工作总结年工作计划(2篇)
- 安全漫画解读讲解
- 运动控糖健康指导手册
- 工地生活区消防安全通知
- 2026年招股说明书撰写能力测试题集
- 音像技术职业发展规划
- 2026年实验室职业健康安全知识培训
- 安全生产验收规范讲解
- 施工会计及基础 1
- DB11-T 513-2025 绿色施工管理规程
- 2025四川泸州交通物流集团有限公司及下属公司招聘12人笔试参考题库附带答案详解
- 危险作业审批培训
- (正式版)DB54∕T 0428-2025 《“一河(湖)一策”方案编制规程》
- 地贫防控知识培训课件
- GB/T 26941-2025隔离栅
- 人工智能概论课程教学大纲
- 2025年江西省中级档案职称考试(档案事业概论)经典试题及答案
- 新疆公务员面试题目及答案
- 物理与现代军事科技
- 2024年广西建设职业技术学院聘用人员招聘考试真题
评论
0/150
提交评论