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("我是子类带二个参数的构造函数");
}
此时编译将不能通过
C#编程(二十三)----------实现继承的更多相关文章
- 二十三、并发编程之深入解析Condition源码
二十三.并发编程之深入解析Condition源码 一.Condition简介 1.Object的wait和notify/notifyAll方法与Condition区别 任何一个java对象都继承于 ...
- (二)Javascript面向对象编程:构造函数的继承
Javascript面向对象编程:构造函数的继承 这个系列的第一部分,主要介绍了如何"封装"数据和方法,以及如何从原型对象生成实例. 今天要介绍的是,对象之间的"继承 ...
- 观察者模式 Observer 发布订阅模式 源 监听 行为型 设计模式(二十三)
观察者模式 Observer 意图 定义对象一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖他的对象都得到通知并自动更新. 别名:依赖(Dependents),发布订阅(Publish-Su ...
- WCF技术剖析之二十三:服务实例(Service Instance)生命周期如何控制[下篇]
原文:WCF技术剖析之二十三:服务实例(Service Instance)生命周期如何控制[下篇] 在[第2篇]中,我们深入剖析了单调(PerCall)模式下WCF对服务实例生命周期的控制,现在我们来 ...
- java GUI编程二
java基础学习总结--GUI编程(二) 一.事件监听 测试代码一: 1 package cn.javastudy.summary; 2 3 import java.awt.*; 4 import j ...
- JS--我发现,原来你是这样的JS:面向对象编程OOP[3]--(JS继承)
一.面向对象编程(继承) 这篇博客是面向对象编程的第三篇,JS继承.继承顾名思义,就是获取父辈的各种"财产"(属性和方法). 怎么实现继承? 我们的JavaScript比较特别了, ...
- (C/C++学习笔记) 二十三. 运行时类型识别
二十三. 运行时类型识别 ● 定义 运行时类型识别(Run-time Type Identification, RTTI) 通过RTTI, 程序能够使用基类的指针或引用来检查(check)这些指针或引 ...
- “全栈2019”Java第五十二章:继承与初始化详解
难度 初级 学习时间 10分钟 适合人群 零基础 开发语言 Java 开发环境 JDK v11 IntelliJ IDEA v2018.3 文章原文链接 "全栈2019"Java第 ...
- 剑指Offer(二十三):二叉搜索树的后序遍历序列
剑指Offer(二十三):二叉搜索树的后序遍历序列 搜索微信公众号:'AI-ming3526'或者'计算机视觉这件小事' 获取更多算法.机器学习干货 csdn:https://blog.csdn.ne ...
- 【读书笔记】C#高级编程 第四章 继承
(一)继承的类型 1.实现继承和接口继承 在面向对象的编程中,有两种截然不同的继承类型:实现继承和接口继承. 实现继承:表示一个类型派生于一个基类型,它拥有该基类型的所有成员字段和函数.在实现继承中, ...
随机推荐
- jenkins+jmeter结合使用
事件背景:想实现jmeter每30分钟执行一次,但是夜里不能人工操作,结果度娘,汇总结果如下 1.配置jmeter测试环境,注意修改Jmeter的bin目录下jmeter.properties文件的配 ...
- ajax调用WebService 不能跨域
http://www.cnblogs.com/dojo-lzz/p/4265637.html "Access-Control-Allow-Origin":'http://local ...
- Vue项目中如何引用外部js
第一种方法:(感觉这个有问题) 1.把需要的js放到static文件夹下 2.在Index.html页面引入 3.在webpack.base.conf.js添加下面代码 externals: { 'W ...
- 深入理解java虚拟机-00
这本书买了有两年了,只有买回来翻了两页...今天电脑有点卡,游戏玩不了了,就来看看这本书. 首先看了序言,这本书是第二版,讲解的jdk版本是1.7,现在公司用的1.8,而且1.8的改动也挺大的,不过在 ...
- web html调用百度地图
如果想在自己的网页上面加入百度地图的话,可以用百度地图的api.具体使用方法如下: 第一步:进入百度创建地图的网站http://api.map.baidu.com/lbsapi/creatmap/,搜 ...
- Scala 学习笔记(2)之类和对象
Scala 的类大抵和 Java 是类似的,简单的例子如下: class MyClass { var myField : Int = 0; def this(value : Int) = { this ...
- 体会 git 之优越性
既生瑜,何生亮.已有subversion,何需git?先有firefox叱咤一时,何需chrome来搅局? 原本以为之前的解决方案已经能够满足现时的需求,但这是真正的事实吗?直到新颖的工具降临,才惊叹 ...
- jQuery事件绑定—on()、bind()与delegate()
啃了一段日子的js相关了,学的过程中发现在jQuery中绑定事件时,有人用bind(),有人用on(),有人用delegate(),还有人用live(),看代码的时候觉得都实现功能了也就掀过去了,只是 ...
- Spark官方文档中推荐的硬件配置
1.关于存储: 1).可能的话,Spark节点与HDFS节点是一一对应的 2).如果做不到,那至少保证Spark节点与HDFS节点是一个局域网内 2.关于硬盘: 1).官方推荐每台机子4-8个硬盘,然 ...
- 032 Spark容错特性
1.spark容错主要分为两个方面 其一是集群 再者为spark的应用程序. 2.Driver 重要补充: driver宕机: Spark On Yarn:总之,要重启 client:只能重启job ...