Object Oriented Programming (3)
本文部分内容节选自Effective C++ by Scott Meyers 和 UML面向对象设计基础 by Meilir Page-Jones。
3 多态
3.1 概述
简单来说,多态(polymorphism)是具有表现多种形态的能力的特征,这使得开发语言具有根据对象的类型以不同方式进行处理的能力。多态意味着许多类可以提供同样的属性或者方法,而且调用者在调用这些属性或方法之前,不必知道某个对象是什么类型。
3.2 虚拟函数
在了解了继承的概念之后, 如果再深入一些,你会发现它分为两类,函数接口(function interface)的继承和函数实现(function implementations)的继承。虚拟函数(virtual function)意味着“接口必须被继承下去”;纯虚拟函数(pure virtual function)则意味着“只有接口必须被继承下去”;而“非虚拟函数”则意味着“接口和实现都必须被继承下去”。
对于一般(非纯)虚拟函数和纯虚拟函数,子类都会继承函数的接口,但不同的是,一般的虚拟函数传统上都会提供一份实现代码,子类可以选择是否加以改写(override)。所以一般(非纯)虚拟函数的目的,是为了让子类继承该函数的接口和缺省行为。但是,允许一般虚拟函数同时指定“函数声明”和“缺省行为”,有时可能会造成危险,考虑一下XYZ航空公司所设计的继承体系,该公司只有两种飞机,A型和B型,两者都以相同的方式飞行,因此继承体系这样设计:
class Airpost{…}; // 机场
class Airplane
{
public:
virtual void fly(const Airpost& destination);
…
}
void Airplane::fly(const Airport& destination)
{
default code for flying
}
class ModelA : public Airplane{…};
class ModelB : public Airplane{…};
为了表示所有飞机都必须能飞,并考虑到“不同型号的飞机原则上需要不同的fly实现代码”,Airplane::fly被声明为virtual。然而为了避免在ModelA和ModelB中撰写相同的代码,缺省的飞行行为由Airplane::fly提供,用时被ModelA和ModelB继承。这个设计突出共同性质,避免代码重复,并提供在未来进行强化的能力,延缓长期维护所需付出--这些都是正式面向对象技术如此收到欢迎的原因。假设XYZ公司利润大增,决定购买一种新的C型飞机。C型飞机和A型以及B型都不相同,更准确的说,它的飞行方式不同。XYZ的开发人员在继承体系中为C型飞机增加了一个class,但由于他们急着让新飞机上线服务,竟忘记重新定义(define)其fly函数。这会造成灾难:因为这个软件试图以ModelA或者ModelB的飞行方式来飞ModelC。
问题并不在于Airplane::fly提供的缺省行为,而是在于ModelC在未明白说“我要使用缺省行为“的情况下继承了该缺省行为,以下是一种解决方法:
class Airplane
{
public:
virtual void fly(const Airpost& destination) = 0;
…
protected:
void defaultFly(const Airport& destnation);
}
class ModelA : public Airplane
{
public:
virtual void fly(const Airport& destination)
{defaultFly(destination);} // inline 调用
…
};
class ModelB : public Airplane
{
public:
virtual void fly(const Airport& destination)
{defaultFly(destination);} // inline 调用
…
};
class ModelC : public Airplane
{
public:
virtual void fly(const Airport& destination)
…
};
void ModelC::fly(const Airport& destination)
{
code for flying ModelC
}
虽然开发人员还是可能因为剪贴代码来实现ModelC而招致麻烦,但是它的确比原先的设计值得信赖。Airplane::defaultFly不是一个虚拟函数,这一点很重要。因为没有任何一个子类应该重新定义此函数。
也有人反对这种以不同的函数分别提供接口和缺省行为,就像上述的fly和defaultFly那样,因为这样会造成class内部命名空间(naming space)的污染。但是他们也同意,接口和缺省行为应该分开,那么这个表面上看起来的矛盾该如何解决?在C++中,可以为纯虚拟函数提供定义(defination),不过调用它的惟一途径是所谓的静态调用,例如:
class Shape
{
public:
virtual void draw() const = 0;
virtual void error(const string& msg);
int objectId() const;
}
class Rectangle : public Shape {…};
class Ellipse : public Shape {…};
Shape *ps = new Rectange;
ps1->Shape::draw();
回到XYZ公司,以下这个新的设计几乎和之前的一样,但是纯虚拟函数Airplane::fly的实现取代了成员函数Airplane::defaultFly。这利用了“纯虚拟函数必须在子类中重新声明(declare),但是纯虚拟函数也可以拥有自己的定义(defination)”这个事实。但是这种设计也有个问题是,丧失了让Airplane::fly的声明和定义有不同的保护级别的机会。
class Airplane
{
public:
virtual void fly(const Airpost& destination) = 0;
…
}
void Airplane::fly(const Airport& destination)
{
//default code for flying
}
class ModelA : public Airplane
{
public:
virtual void fly(const Airport& destination)
{Airplane::fly(destination);}
…
};
class ModelB : public Airplane
{
public:
virtual void fly(const Airport& destination)
{ Airplane:: (destination);}
…
};
class ModelC : public Airplane
{
public:
virtual void fly(const Airport& destination)
…
};
void ModelC::fly(const Airport& destination)
{
//code for flying ModelC
}
有了以上知识,当你声明你的成员函数(member function)的时候,就必须谨慎选择,不要把所有的成员函数都声明非虚拟函数,这使得子类没有足够的空间进行特殊化工作。非虚拟的析构函数(destructors)尤其会带来问题。同时也不要把所有的成员函数都声明成虚拟函数(请思考Java是怎么做的?),因为对于一个继承体系而言,有些行为是不会变化的,如果你想确保“不变性”,那么就使用非虚拟函数。
推荐阅读
-
PHP学习记录之面向对象(Object-oriented programming,OOP)基础【类、对象、继承等】
-
通过CSS3的object-fit来调整图片适配尺寸的技巧简介
-
2020年3月21日Benelux Algorithm Programming Contest 2019
-
YOLOv3 object detection demo
-
python3_opencv_直线检测_HoughLinesP_ 'NoneType' object is not subscriptable
-
gmx-MMPBSA — error while loading shared libraries: libgfortran.so.3: cannot open shared object file
-
使用 Object.defineProperty (vue2)和 Proxy(vue3)实现Vue双向数据绑定
-
多线程(3)-基于Object的线程等待与唤醒
-
Pseudo-LiDAR from Visual Depth Estimation: Bridging the Gap in 3D Object Detection for Autonomous Dr
-
面向对象编程Object-Oriented和装饰器decorator