C#编程(二十三)----------实现继承

原文链接:http://blog.csdn.net/shanyongxu/article/details/46593809

如果要声明派生自另一个类的一个类,可以使用下面的语法:

class DerivedClass: BaseClass

{

//function and data members here

}

这个语法类似于C++和Java中的语法,但是,C++程序员习惯使用公共和私有继承的概念;注意C#不支持私有继承,因此在基类名上没有public或者private限定符.支持私有继承指挥大大增加语言的复杂性,实际上私有继承在C++中也很少使用.

如果类(或结构)也派生子接口,则使用逗号分隔列表中的基类和接口.

public class DerivedClass: BaseClass,InterFace1,Interface2

{

}

对于结构:

public struct DerivedClass: Interface1,Interface2

{}

如果在类中没有定义基类,C#编译器就假定System.Object是基类.例如:

class MyClass:Object

{}

class MyClasst

{}

这两种方式是相同的结果,第二种方式比较常用,因为比较简单.C#支持object关键字,它用作System.Object类的假名,所以可以这么写:

class MyClass : object

{}

如果要引用Object类,就可以使用object关键字,VS会识别它.

虚方法

把一个基类函数声明为virtual,就可以在任何派生类中重写该函数:

class BaseClass

{

public virtual string VirtualMethod()

{

return “the method is virtual and defined int BaseClass”;

}

}

也可以把属性声明为virtual.对于虚属性或重写属性,语法和非虚属性相同,但是要在定义中天剑virtual关键字,语法如下:

public virtual string foreName

{

get{return foreName;}

set{foreName=value;}

}

虚方法的规则同样适用于虚属性.可以在派生来中重写虚函数.在调用方法时,会调用该类对象的合适方法.在C#中,函数在默认情况下不是虚拟的,但是(除了构造函数以外)可以显示的声明virtual.这遵循C++的方法,即从性能的角度来看,除非显式指定,否则函数就不是虚函数.而在JAVA中,所有的函数都是虚拟的.单C#和C++的语法不通,因为C#要求在派生类的函数重写另一个函数时,要使用override关键字现实生命.

例如:

class DerivedClass: BaseClass

{

public override string VirtualMethod()

{

return “this is an override defined in DerivedClass”;

}

}

重写方法的语法避免了C++中很容易发生的潜在运行错误:当派生类的方法签名无意中与基类版本略有差别时,该方法就不能重写基类的方法.在C#中,者会出现一个编译错误,因为编译器会认为函数已标记为override,单没有重写其基类的方法.

成员字段和静态函数都不能生命为virtual,因为这个概念只对类中的函数成员有意义.

例如:

class BaseClass

{

public virtual string fun()

{

return "BaseClass method";

}

}

class DerivedClass : BaseClass

{

public override string fun(string str)

{

return "DerivedClass method";

}

}

隐藏方法

如果签名相同的方法在基类和派生类中都进行了声明,但是该方法没有声明为virtual和override,派生类方法就会隐藏基类方法.

大多数情况下,是要重写方法,而不是隐藏方法,因为隐藏方法会造成对于给定类的实例调用错误方法的危险.但是,如下例,C#语法可以确保开发人员在编译时受到这个潜在错误的警告,从而使隐藏方法更加安全.

假定有一个类HisBaseClass:

class HisBaseClass

{

//various members

}

在将来的某一刻,要编写一个派生类,用它给HisbaseClass添加某个功能,特别是要添加该基类中目前没有的方法----MyMethod():

class MyDerivedClass:HisBaseClass

{

public int MyMethod()

{

//something

return 0;

}

}

一年后,基类的编写者决定扩展基类的额功能.为了保持一致,他也添加一个名为MyMethod()的方法,该方法的名称和签名玉前面添加的方法相同,但是并不完成相同的工作.在使用基类的新方法编译代码时,程序在应该调用那个方法上就会有潜在的冲突.这在C#中完全合法,但因为MyMethod()与基类的MyMethod()不相关,运行这段代码就可能会产生以外的结果.C#可以很好地处理这种冲突.

此时,编译时系统会发出警告.在C#中,要隐藏一个方法应使用new关键字,语法如下:

class MyDerivedClass : HisBaseClass

{

public new int MyMethod()

{

//something

return 0;

}

}

但是新添加的MyMethod()没有生命为new,所以编译器会认为他隐藏了基类的方法,但没有显式声明,因此系统会发出一个警告(这也适用于是否把MyMethod()声明为vritual).如果愿意,就可以给新方法重命名.最好这么做,因为这会避免许多冲突,但是,如果觉得重命名方法不可能(例如,已经针对其他公司把软件发布为一个库,所以无法修改方法的名称),则所有的已有哭护短代码仍能正常运行,方法是选择新添加的MyMethod().这是因为访问这个方法的任何已有代码必须通过对MyDerivedClass(或进一步派生的类)的引用进行选择.

已有的代码不能通过对HisBaseClass类的引用方法这个方法,因为在对HisBaseClass类的早期版本进行编译时,会产生一个编译错误.这个问题只会发生在将来编写的客户端代码上.C#会发出一个警告,告诉用户在将来的代码中可能会出现问题----用户应该注意这个警告,不要试图在将来天机的代码中通过对HisBaseClass的引用调用新的MyMethod()方法,但所有已有的代码仍会正常工作.这是比较微妙的,但它很好地说明了C#如何处理类的不同版本.

调用函数的基类版本

C#有一种特殊的语法用语从派生类中调用方法的基类版本:base.方法名.例如,假定派生类中的一个方法要返回基类的方法90%的返回值,就可以使用下面的语法:

class CustomerAccount

{

public virtual decimal CalculatePrice()

{

return 0.0m;

}

}

class GoldAccount:CustomerAccount

{

public override decimal CalculatePrice()

{

return base.CalculatePrice()*0.9m;

}

}

注意,可以使用base.方法名语法调用基类中的任何方法,不必从同一个方法的重载中调用它.

抽象类和抽象函数

使用关键字abstract.C#允许把类和函数声明为abstract.抽象类不能实例化,而抽象函数不能直接调用,必须在非抽象的派生类中重写.显然,抽象函数本身也是虚函数(尽管不需要提供vritual关键字,实际上,如果提供了vritual关键字,就会产生一个语法错误).如果类包含抽象函数,则该类也是抽象的,必须声明为抽象的:

abstract class Building

{

public abstract decimal CalculateHeatingCost();//抽象函数

}

C++开发文员还要注意术语上的细微差别:在C++中,抽象函数常常描述为纯虚函数,而在C#中,仅使用抽象这个术语.

密封类和密封方法

C#允许把类和方法声明为sealed.对于类,这表示不能继承该类;对于方法,表示不能重写该方法.

sealed class FinaClass

{

}

class DerivedClass:FinaClass//这是错误的

{

}

在把类或方法标记为sealed时,最可能的情形是:如果要对库,类或自己编写的其他类作用域之外的类或方法进行操作,则重写某些功能导致代码混乱.一般情况下,把类或成员标记为sealed是要小心,因为这么做会严重限制他的使用方式.及时认为他不能对继承自一个类或重写类的某个成员发挥作用,仍有可能在将来的某个时刻,有人会遇到我们没有预料到的情况,此时这么做很有用..NET基类库大量使用了密封类,是希望从这些类中派生出自己的类的第三方开发人员无法访问这些类.例如:string就是一个密封类

clas MyClass:MyClassBase

{

public sealed override void FinaMethod()

{}

}

class DerivedClass : MyClass

{

public override void FinaMethod();//错误

}

要在方法或属性上使用sealed关键字,必须先从基类上把它声明为要重写的方法或者属性.如果基类不希望有重写方法或属性,就不要把它声明为vritual.

派生类的构造函数

先来看这样一段代码:

abstract class GenericCustomer

{

private string name;

}

class Nevermore60Customer:GenericCustomer

{

private uint hishCostMinutesUsed;

}

构造函数的调用顺序实现调用System.Object,再按照层次结构由上向下进行,指导到大编译器要实例化的类为止,还要注意在这个过程中,每个构造函数都初始化它自己的类中的字段.这是它的一般工作方式,再开始添加自己的构造函数时,也应尽可能的遵循这条规则.

注意构造函数的执行顺序.总实现调用的正是基类的额构造函数.也就是说,派生类的构造函数可以在执行过程中调用它可以访问的任何积累方法,属性和任何其他成员.因为基类已经构造出来了,其字段也初始化了,这也意味着,如果派生类不喜欢初始化基类的方式,但要访问数据,就可以改变数据的初始值.但是,好的编程方式是让基类构造函数来处理其字段.

首先来看最简单的情况,在层次结构中用一个无参数的构造函数来替换默认的构造函数后,看看会出现什么情况.假定要把每个人的名字初始化为字符串”<no name>”,而不是null引用.可以这样:

public abstract class GenericCustomer

{

private string name;

public GenericCustomer()

:base()

{

name=”<no name>”;

}

}

添加这段代码之后,代码运行正常.Nevermore60Customer仍有自己的默认构造函数,所以上面描述的事件的顺序保持不变,但编译器会使用自定义的GenericCustomer构造函数,而不是生成默认的构造函数,所以那么字段按照需要总是初始化为”<no name>”.

这次使用的关键字是base,而不是this,表示这是基类的构造函数,而不是要调用的当前的构造函数.在base关键字后面的圆括号中没有参数,这非常重要,因为没有给基类构造函数传送任何采纳数,所以编译器必须调用无参数的构造函数.其结果是编译器会插入要调用的System.Object构造函数的代码,这正好与默认情况相同.

实际上可以省略这行代码:

public GenericCustomer()

{

name=”<no name>”;

}

base和this关键字是调用另一个构造函数时允许使用的唯一关键字,其他关键字都会产生编译错误.还要注意只能指定唯一一个其他的构造函数.

但是如果这样:

private GenericCustomer()

{

name=”<no name>”;

}

把构造函数声明为私有的,就会产生编译错误.有趣的是,该错误不是发生在GenericCustomer类中,而是发生在Nevermore60Customer派生类中.原因是:编译器试图为Nevermore60Customer生成默认的构造函数,但是又做不到,因为默认的构造函数应调用无参数的GenericCustomer构造函数.把该构造函数声明为private,他就不鞥呢访问派生类了.如果为GenericCustomer提供一个带参数的构造函数,但同时没有提供一个无参数的构造函数,也会发生类似的错误.在本例中,编译器不能为GenericCustomer生成默认构造函数,所以当编译器试图为任何派生类生成默认的构造函数时,会砸死次发现他不能做到这一点,因为没有无参数的基类构造函数可以调用.解决办法是为派生类添加自己的构造函数---实际上不需要在这些构造函数中做任何工作,这样便一起就不会为这些派生类生成任何默认的构造函数了.

在层次结构中添加带参数的构造函数

首先是带一个参数的GenericCustomer构造函数,仅在顾客提供其姓名的时候才实例化顾客.

abstract class GenericCustomer

{

private string name;

public GenericCustomer(string name)

{

this.name=name;

}

}

刚才说过,在编译器试图为派生类创建默认的构造函数时,会产生一个编译错误,因为编译器为Nevermore60Customer生成的默认是构造函数会试图调用一个无参数的GenericCustomer构造函数,但是GenericCustomer中没有这样的构造函数.因为,需要为派生类提供一个构造函数,来避免这个错误.

class Nevermore60Customer:GenericCustomer

{

private uint highCostMinutesUsed;

public Nevermore60Customer(string name)

:base(name)

{}

}

现在,Nevermore60Customer对象的实例化只有在提供了包含顾客姓名的字符串时才能进行,这正是我们需要的.有趣的是Nevermore60Customer构造函数对这个字符串所做的处理.他本身不能初始化name字段,因为他不能访问基类中的私有字段,但可以把顾客姓名传送给基类,以便GenericCustomer构造函数处理.具体方法是,把先执行的基类构造函数指定为顾客姓名当做参数构造函数.除此之外,不需要执行任何操作.

再来看如下代码:

class Nevermore60Customer:GenericCustomer

{

public Nevermore60Customer(string name,string referrerName)

:base(name)

{

this.referrerName=referrerName;

}

private string referrerName;

private uint highCostMinutesUsed;

}

构造函数将姓名作为参数,把他传递给GenericCustomer构造函数进行处理.referrerName是一个需要声明的变量,这样构造函数才能在其主题中处理这个参数.

但是并不是所有人都有联系人(referrerName),所以看下面的代码:

public Nevermore60Customer(stiring name)

:this(name,”<none>”)

{

}

这样就正确的建立了所有的构造函数.执行下面的代码:

GenericCustomer customer=new Nevermore60Customer(“syx”);

比哪一期认为他需要一个字符串参数的构造函数,所以他确认的构造函数是:

public Nevermore60Customer(stiring name)

:this(name,”<none>”)

{

}

在实例化customer时,就会调用这个构造函数.之后立即把控制权传递给对应的Nevermore60Customer构造函数,该构造函数有两个参数,分别是”syx”和”<none>”.在这个构造函数中,把控制权依次传递给GenericCustomer构造函数,该构造函数有一个参数,即字符串”syx”.然后这个构造函数会把控制权传递给System.Object默认构造函数,现在才能执行这些构造函数,首先执行object的构造函数,接着执行genericCustomer的构造函数,它初始化name字段,然后带有两个参数的Nevermore60Customer的构造函数得到控制权,把联系人(referrerName)的姓名初始化”<none>”.最后,执行Nevermore60Customer构造函数,该构造函数带有一个参数----这个构造函数什么也不做.

该过程很合理,也很简洁.每个构造函数都负责处理变量的初始化,在这个过程中,正确的实例化了类,以备使用.如果再为类编写自己的构造函数时遵循同样的规则,就会发现,即使是最复杂的类也可以顺利的初始化.

最后总结一下:

1、  当基类中没有自己编写构造函数时,派生类默认的调用基类的默认构造函数

Ex:

public class MyBaseClass

{

}

public class MyDerivedClass : MyBaseClass

{

public MyDerivedClass()

{

Console.WriteLine("我是子类无参构造函数");

}

public MyDerivedClass(int i)

{

Console.WriteLine("我是子类带一个参数的构造函数");

}

public MyDerivedClass(int i, int j)

{

Console.WriteLine("我是子类带二个参数的构造函数");

}

}

此时实例化派生类时,调用基类默认构造函数

2、  当基类中编写构造函数时,派生类没有指定调用构造哪个构造函数时,会寻找无参的构造函数,如果没有则报错,另外无论调用派生类中的哪个构造函数都是寻找无参的那个基类构造函数,而非参数匹配。

Ex:

public class MyBaseClass

{

public MyBaseClass(int i)

{

Console.WriteLine("我是基类带一个参数的构造函数");

}

}

public class MyDerivedClass : MyBaseClass

{

public MyDerivedClass()

{

Console.WriteLine("我是子类无参构造函数");

}

public MyDerivedClass(int i)

{

Console.WriteLine("我是子类带一个参数的构造函数");

}

public MyDerivedClass(int i, int j)

{

Console.WriteLine("我是子类带二个参数的构造函数");

}

}

此时实例化派生类时则报错

3、  基类中编写了构造函数,则派生类中可以指定调用基类的某个构造函数,使用base关键字。

Ex

public class MyBaseClass

{

public MyBaseClass(int i)

{

Console.WriteLine("我是基类带一个参数的构造函数");

}

}

public class MyDerivedClass : MyBaseClass

{

public MyDerivedClass() : base(i)

{

Console.WriteLine("我是子类无参构造函数");

}

public MyDerivedClass(int i) : base(i)

{

Console.WriteLine("我是子类带一个参数的构造函数");

}

public MyDerivedClass(int i, int j) : base(i)

{

Console.WriteLine("我是子类带二个参数的构造函数");

}

}

此时实例化派生类时使用的带一个参数的构造函数时,则不会报错,因为他指定了基类的构造函数。

4、  如果基类中的构造函数不含有无参构造函数,那么派生类中的构造函数必须全部指定调用的基类构造函数,否则出错

Ex

public class MyBaseClass

{

public MyBaseClass(int i)

{

Console.WriteLine("我是基类带一个参数的构造函数");

}

}

public class MyDerivedClass : MyBaseClass

{

public MyDerivedClass()

{

Console.WriteLine("我是子类无参构造函数");

}

public MyDerivedClass(int i) : base(i)

{

Console.WriteLine("我是子类带一个参数的构造函数");

}

public MyDerivedClass(int i, int j)

{

Console.WriteLine("我是子类带二个参数的构造函数");

}

此时编译将不能通过

时间: 2024-10-07 05:31:28

C#编程(二十三)----------实现继承的相关文章

C++ Primer 学习笔记_31_面向对象编程(2)--继承(二):继承与构造函数、派生类到基类的转换 、基类到派生类的转换

C++ Primer 学习笔记_31_面向对象编程(2)--继承(二):继承与构造函数.派生类到基类的转换 .基类到派生类的转换 一.不能自动继承的成员函数 构造函数 拷贝构造函数 析构函数 =运算符 二.继承与构造函数 基类的构造函数不被继承,派生类中需要声明自己的构造函数. 声明构造函数时,只需要对本类中新增成员进行初始化,对继承来的基类成员的初始化调用基类构造函数完成(如果没有给出则默认调用默认构造函数). 派生类的构造函数需要给基类的构造函数传递参数 #include <iostream

ActionScript3游戏中的图像编程(连载二十三)

2.1 Photoshop图层样式初体验 打开Photoshop CS5,新建一个600*100(单位:像素)的空白文档. 使用“横排文字工具”在画布上拖出一个文本框,字体设置为微软雅黑并加粗(最好是使用直接的粗体,而非仿粗),字号80号,字体颜色#0000FF,文字内容为“Flash艺术编程”. 无法正确完成以上任务的初学者可自行查阅Photoshop的基础教程进行学习. 在图层面板(如果图层面板没打开,就可通过菜单项“窗口”——“菜单”或者按F7调出来)右击文字所在的层,选择 “混合选项”菜

算法系列之二十三:离散傅立叶变换之音频播放与频谱显示

算法系列之二十三:离散傅立叶变换之音频播放与频谱显示 算法系列之二十三离散傅立叶变换之音频播放与频谱显示 导语 什么是频谱 1 频谱的原理 2 频谱的选择 3 频谱的计算 显示动态频谱 1 实现方法 2 杂项说明 结果展示 导语 频谱和均衡器,几乎是媒体播放程序的必备物件,没有这两个功能的媒体播放程序会被认为不够专业,现在主流的播放器都具备这两个功能,foobar 2000的十八段均衡器就曾经让很多人着迷.在上一篇对离散傅立叶变换介绍的基础上,本篇就进一步介绍一下频谱是怎么回事儿,下一篇继续介绍

【读书笔记】C#高级编程 第四章 继承

(一)继承的类型 1.实现继承和接口继承 在面向对象的编程中,有两种截然不同的继承类型:实现继承和接口继承. 实现继承:表示一个类型派生于一个基类型,它拥有该基类型的所有成员字段和函数.在实现继承中,派生类型采用基类型的每个函数代码,除非在派生类型的定义中指定重写某个函数的实现代码.在需要给现有的类型添加功能,或许多相关的类型共享一组重要的公共功能时,这种类型的继承非常有用. 接口继承:表示一个类型只继承了函数的签名,没有继承任何实现代码.在需要制定该类型具有某些可用的特性时,最好使用这种类型的

QT开发(二十三)——软件开发流程

QT开发(二十三)--软件开发流程 一.软件开发流程简介 软件开发流程是通过一系列步骤保证软件产品的顺利完成,是软件产品在生命周期内的管理学. 软件开发流程的本质是软件开发流程与具体技术无关,是开发团队必须遵守开的规则. 二.常见软件开发流程模型 常见的软件开发流程模型包括即兴模型.瀑布模型.增量模型.螺旋模型.敏捷模型. 1.即兴模型 即兴模型的特点: A.与用户交流后立即进行开发 B.没有需求分析和需求发掘过程 C.没有整体设计和规划 D.没有软件文档,可维护性差 2.瀑布模型 瀑布模型的特

java基础学习总结——GUI编程(二)

永不放弃,一切皆有可能!!! 只为成功找方法,不为失败找借口! java基础学习总结——GUI编程(二) 一.事件监听 测试代码一: 1 package cn.javastudy.summary; 2 3 import java.awt.*; 4 import java.awt.event.*; 5 6 public class TestActionEvent { 7 public static void main(String args[]) { 8 Frame f = new Frame("

C++ Primer 学习笔记_71_面向对象编程 --句柄类与继承

面向对象编程 --句柄类与继承 引言: C++中面向对象编程的一个颇具讽刺意味的地方是:不能使用对象支持面向对象编程,相反,必须使用指针或引用. void get_prices(Item_base object, Item_base *pointer, Item_base &reference){ //需要根据指针或引用实际所绑定的类型进行调用 cout<< pointer ->net_price(1)<< endl; cout<< reference.n

Swift学习笔记十三:继承

一个类可以继承(inherit)另一个类的方法(methods),属性(property)和其它特性 一.基本语法 class Human{ var name :String init(){ name = "human" println(name) } func description(){ println("name:\(name)") } } class Student:Human{ var score = 0 init(){ super.init() name

Android项目实战(二十三):仿QQ设置App全局字体大小

原文:Android项目实战(二十三):仿QQ设置App全局字体大小 一.项目需求: 因为产品对象用于中老年人,所以产品设计添加了APP全局字体调整大小功能. 这里仿做QQ设置字体大小的功能. QQ实现的效果是,滚动下面的seekbar,当只有seekbar到达某一个刻度的时候,这时候上部分的效果展示部分会改变文字大小, 但是在拖动过程中字体不会改变.关闭此界面,就可以看到改变文字后app整体的实际文字大小效果了. ----------------------------------------

winform学习日志(二十三)---------------socket(TCP)发送文件

一:由于在上一个随笔的基础之上拓展的所以直接上代码,客户端: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Net.Sockets; using Sys