第六章 继承和多态
本章关注OOP两个支柱,继承和多态。首先学习了如何利用继承来构建一族相关类,然后学写了虚成员和抽象成员在类层次结构中创建多态接口,最后介绍了超级父类System.Object的作用。
6.1继承
在OOP中,代码重用有两类,一种是经典继承(is-a的关系),另一种是包含/委托模型(has-a关系),前者就是通常所说的继承。
经典继承基本思想就是新的类可以利用或扩展既有类的功能。类定义中利用冒号运算符(:)在类之间创建继承关系。子类将拥有父类的每一个公共成员的访问权限。即使子类没有增加任何成员,也可以直接访问父类的公共成员,从而实现代码重用。注意,子类无法访问父类的任何私有成员哦。
基类用于定义所有派生类型都共有的一般特征。子类通过增加特定的行为来扩展这些一般功能。
在.NET中,要求每一个类只能有一个直接基类。不支持多重继承。但是允许某个类或者结构类型实现许多独立的接口,这样,C#类型可以实现很多行为,同时避免了多重继承引起的复杂性。还有,虽然只能直接继承一个基类,但是一个接口可以直接从多个接口派生,因此可以构建灵活的接口层次来建模复杂的行为。另外,系统提供了sealed关键字来防止发生继承。一方面因为有许多工具类应该定义为封闭的,另一方面,有些类在被继承多层之后,不想再被继续继承,也可以使用此功能。
在继承关系链上,可以使用base关键字,主要有两个作用:
(1)优化派生类构造函数
一般基类的默认构造函数会在派生类构造函数执行之前被自动调用。为了帮助优化派生类的创建,最好实现子类构造函数来显式调用合适的自定义基类构造函数而不是默认构造函数,这样,可以减少对继承的初始化成员的调用次数。(也就节省了时间),使用方法和使用this来在构造函数链上进行调用类似,代表派生构造函数将数据传递到最近的父构造函数中(上一级的)。
public manager(string fullname,int age):base(fullname)
{}
这样,就调用了基类中参数为fullname的构造函数哦!
疑问:可不可以在构造函数上同时使用this和base呢?怎么写?
(2)使用父类的公共成员
base不是只有构造函数逻辑才能使用,只要子类想访问由父类定义的公共或受保护的成员,都可以使用base关键字。如:
public sales()
{
base.fun1();
}
注意两种用途的使用方法略有不同哦。
这里需要强调的是,基类访问(base)只能在构造函数、实例方法或实例属性访问器中进行。
从静态方法中使用 base 关键字是错误的。
protected关键字:
当基类定义了受保护数据或受保护成员后,他就创建一组可以直接被任何后代访问的项。对于家族以外的对象,受保护数据时私有的,无法访问!
sealed关键字:
密封类是不能被其他类扩展的。如果希望构建新类来使用密封类的功能,唯一办法就是放弃经典继承,使用包含/委托模型。值类型就是密封的,不允许继承。另外,sealed可以修饰方法。
包含/委托模型:
包含就是在一个类的定义中,实例化另外一个类,例如:
class benefitpackage
{
protected benefitpackage emp=new benefitpackage();
}
至此,已经包含了另外一个对象,并在类内部使用它。
然而,若要公开被包含对象的功能给外部世界,就需要委托。简单说,委托就是增加成员到包含类,以便使用被包含对象的功能,例如在上面代码基础上,增加:
public double getbenefitcost()
{
return emp.computepaydeduction();
}
public benefitpackage benefits
{
set{mepbenefits=value;}
get{return mepbenefits;}
}
至此,就可以让外部世界访问这个包含对象啦。
嵌套类型:
嵌套类型是C#中,可以直接在类或结构作用域中定义类型(枚举、类、接口、结构或委托),如果这样,被嵌套(或内部)类型被认为是嵌套类(或外部)类的成员。可以像操作其他成员一样来操作嵌套类型。
由于它被认为是成员,所以具有如下特征:
- 凡是控制成员的访问修饰符,嵌套类型也可以使用;
因此,别吃惊可以出现private class...,因为这个类是嵌套类型,而不是独立的类。由于当作了成员,因此默认为隐式私有的。但是可以设置为 public、protected internal、protected、internal 或 private。
- 可以访问包含类的私有成员
因为他是在包含类当中,而不是外部。
- 通常只用作外部类的辅助方法,而不是为外部世界所准备。
否则,干脆定义为独立的类型,干嘛要嵌套。
关于嵌套类型,确实有点儿复杂,它既有类型的特点,也有成员的性质,请参考如下文章:
http://msdn.microsoft.com/en-us/library/ms173120(zh-cn,VS.80).aspx
http://www.cnblogs.com/MyLoverLanlan/archive/2009/01/06/1370745.html
6.2多态
多态的性质使得相关类型对相同的请求做出不同的响应。它为子类提供一种方式,使其可以自定义有其基类定义的方法,这个过程就叫做重写。(区分于重载哦),为了达到修改当前设计的目的,需要理解virtual和override关键字的作用。
如果基类希望定义的方法可以而不是必须由子类重写,就应该在基类中指定方法是虚的(virtual)。在子类中,如果子类希望改变虚方法的实现细节,就必须使用override关键字。
需要注意的是,既然override是重写方法的实现,那么重写的方法名、返回类型和参数都必须完全一致哦!另外,override的方法不是在基类中声明为virtual或abstract的,也是无法使用的,还有,virtual或abstract的方法必须是公共的,因为私有的方法是无法被重写的。
另外,重写的可以是方法,也可以是属性,因为属性本质上还是方法!
之前说过sealed关键字修饰一个类时,为了使类无法被继承,那么有时候不想让类中某个方法被继续重写,就需要用sealed修饰方法啦。这时必须是sealed和override同时使用:
public override sealed test()
{}
这个方法重写了由基类继承来的方法,并且要求之后子类再被继承时,这个方法不允许再被重写啦。
抽象类:
若希望类不能被实例化,而只能被继承,就需要将类声明为abstract,如果被定义为抽象基类,他就可以定义许多抽象成员,要定义没有提供默认实现的成员时就可以使用抽象成员,这就强制每一个后代具有多态接口,他们需要自己处理抽象方法的细节。简言之,抽象基类的多态接口是指一组虚的或者抽象的方法。
注意,抽象类中可以有非抽象方法(普通方法和虚拟方法)和抽象方法,其中抽象方法只能在抽象类中声明,否则编译错误。
注意:类可以是抽象类或密封类,抽象方法必须在抽象类中,密封方法可以在任何类中。
6.3基类与子类关系与转换
成员投影:
与重写相对的功能,就是投影。如果派生类定义的成员和定义在基类中的成员(字段、常量、静态方法、属性等)一致,派生类就投影(覆盖)了父类的版本。需要使用new关键字修饰子类的成员来实现投影。从而显式声明派生类型的实现故意设计为隐藏父类的版本。
注意,即使是投影虚拟或抽象方法都是可以的,这其实和使用override是一个意思了。当然,即使覆盖了基类的成员,依然可以使用显式强制转换来触发阴影成员(子类成员)在基类中的实现:
circle mycircles=new circle();
mycircles.show();//假设show是用new投影的方法
((shape)mycircles).show();//调用父类的show方法,强制转换
基类、派生类转换规则:
转换分为隐式转换和显式转换(或叫强制转换),同样,也就是两个规则:
(1)如果两个类是通过“is-a”关系关联,在基类引用中指向派生类型总是安全的,因为它基于继承的规则,也就是向上转换,子类转换为了父类类型。
shape myshapes=new circle();//circle “is-a” shape
object myobjects=new shape();// shape "is-a" object
看出规律是,等号后必须是等号前的子类。子类转换为父类,必然失去了子类所特有的东西,因为已经成为了父类。不要认为“丢失东西”的转换不可能系统也可以隐式转换哦。这些转换在本质上与常规的转换不同,这种转换实际上并没有对对象进行任何数据转换。如果要进行的转换是合法的,它们也仅是把新引用设置为对对象的引用。
之所以转换可以,这是因为对类shape的任何引用都可以引用类shape的对象或派生于shape的对象。在OO编程中,派生类的实例实际上是基类的实例,但加上了一些额外的信息。因此,以下语句的前三个是可以隐式转换的,但是第四个转换若想转换成功是有条件的,这就是下一条规则。
shape derivedObject = new circle();
shape baseObject = new shape();
circle derivedCopy1 = (circle) derivedObject; // OK
circle derivedCopy2 = (circle) baseObject; // Throws exception
同理,一个方法的参数可以传递这个参数类型及其所有子类型均为合法。
(2)必须使用C#强制转换运算符(小括号)进行显式的向下转换。也就是父类强制转换为子类:
circle derivedCopy2 = (circle) baseObject;
疑问:不知道为什么本书没有提及:实际上转换在运行时还是会出错的,严格来说,父类是不能转换为子类的,子类包含了父类所有的方法和属性,而父类则未必具有和子类同样成员范围,所以这种转换是不被允许的,即便是两个具有父子关系的空类型,当然,你可以写出一些看似由父类转换为子类的实例,但是这种转换是毫无意义的。按照Liskov替换原则,父类转换为子类是对OOP的严重违反,不提倡、也不建议。
因此,一个真正的父类是无法转换为子类的,首先隐式的转换肯定是编译就出错的,其次,即使是强制转换了,编译不出错,但是运行时也会出错,那么这个转换到底有什么意义呢?
这就是只有一种情况才可以强制转换成功(编译、运行时均成功)就是:
父类本来就是由子类转换来的,再转换回去(或继续往上转换),是没问题的。
比如类之间关系为a,b,c(c继承b,b继承a),则如果一个类是从c转换过来的成为a,则这个c对象是可以强制转换为b甚至是c类型的,再看下面代码:
Classchil b = new Classchil();//Classpar是父类,Classchil为子类
Classpar c = new Classchil();//c是由Classchil转换过来的父类
b = (Classchil)c;//自然,c这个父类是可以转换回原来的自己,子类的。
类似的应用就是如果方法参数是父类的,那么如果传递过来一个子类(这当然是允许的),实际上这个子类被隐式转换为父类了,那么在方法中,若经过判断是子类了,要调用子类的成员,那么就应该将这个参数转换回子类(强制转换),再调用相应子类成员。
若检测一个强制类型转换是否成功,常规方法是通过try-catch来捕获可能的异常,但是对于这种类型转换,c#提供了以下两个关键字来判断:
as关键字:
as关键字在运行时快速检测某个类型是否和另外一个兼容,可以通过检查null返回值来检测兼容性(是否转换成功)。当然转换成功条件还是上面的黑体字。
as的规则如下:
- 检查对象类型的兼容性,并返回结果,如果不兼容就返回null;
- 不会抛出异常。
circle derivedCopy2 = baseObject as circle ; //其中baseObject 为父类对象
if(derivedCopy2 ==null)
{}
is关键字:
提供了is关键字来检测两个项是否兼容(条件还是上面的黑体字),和as不同,如果类型不兼容,is返回false而不是null。
is的规则如下:
- 检查对象类型的兼容性,并返回结果,true或者false;
- 不会抛出异常;
- 如果对象为null,则返回值永远为false。
if (baseObject is circle)
{
circle derivedCopy2 = (circle )baseObject;
}
注意,这句话要经过判断,如果兼容才继续执行强制转换。
综上比较,is/as操作符,提供了更加灵活的类型转型方式,as是先转换后判断,is是先判断,后转换。as操作符在执行效率上更胜一筹,我们在实际的编程中应该体会其异同,酌情量才。
6.4超级父类System.Object
.NET世界中,所有的类型最终都会从System.Object的基类派生。如果我们构建的类没有显式定义其基类,编译器会自动从Object派生我们的类型,那么下面的冒号和object是可以没有的:
class car:object
{}
object定义了一组成员。某些被声明为虚的,某些是实例级别的,某些是静态成员。
疑问:在书中第168页,说object中有虚成员Finalize,但是2008中我的怎么没有这个啦?
没有太多需要强调的,有几个概念需要记住:
在《Effective C#》所有的50个建议中唯一一项关于不推荐函数的建议。GetHashCode()这个方法只会用于一个地方:给基于Hash的Collection(比如HashTable和Dictionary)的Key定义Hash值,也就是对象做为Key,而对象的GetHashCode()为该Key获取Hash值。
MemberwiseClone 方法创建一个浅表副本,具体来说就是创建一个新对象,然后将当前对象的非静态字段复制到该新对象。如果字段是值类型的,则对该字段执行逐位复制。如果字段是引用类型,则复制引用但不复制引用的对象;因此,原始对象及其复本引用同一对象。
6.5 总结
这里根据上一章和本章,总结一下用于修饰类的关键字:
- public :公共类
- internal:内部类
- partial:分部类
- abstract:抽象类
- sealed:密封类
- static:静态类
public和internal不能同时修饰一个类,abstract、sealed和static不能同时修饰同一个类。其他情况可以任意搭配,且除了partial必须紧挨着类型(class、struct、interface)其他的顺序没有要求。如:
sealed public partial class Cla{}、
然后总结一下可以修饰成员的(方法、属性)的关键字:
- public
- private
- internal
- protected
- protected internal
- abstract
- vitrual
- override
- new
- static
- stealed override
public、private、internal、protected 、protected internal不能同时使用。abstract、vitrual、override、new、static、stealed override不能同时存在。override或stealed override的方法必须是父类中的abstract或virtual或已经override的方法,且不能改变父类的访问修饰符,但是和其他关键字的顺序可以改变。abstract的方法必须在abstract类中才能使用。
总结一下可以修饰成员变量的关键字:
- public
- private
- internal
- protected
- protected internal
- new
- static
public、private、internal、protected 、protected internal不能同时使用。new和static不能同时存在。
关于new和override的区别,可以参考:
http://www.cnblogs.com/OpenCoder/archive/2009/11/20/1607225.html