扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
现在,假设 XYZ 公司的财富增长了,决定引进一种新机型,Model C。Model C 在某些方面与 Model A 和 Model B 不同。特别是,它的飞行不同。
XYZ 公司的程序员在 hierarchy(继承体系)中增加了 Model C 的 class,但是由于他们匆匆忙忙地让新的机型投入服务,他们忘记了重定义 fly function:
class ModelC: public Airplane {
... // no fly function is declared
};
于是,在他们的代码中,就出现了类似这样的东西:
Airport PDX(...); // PDX is the airport near my home
Airplane *pa = new ModelC;
...
pa->fly(PDX); // calls Airplane::fly!
这是一个灾难:企图让一个 ModelC object 像一个 ModelA 或 ModelB 一样飞行。这在旅行人群中可不是一种鼓舞人心的行为。
这里的问题并不在于 Airplane::fly 有缺省的行为,而是在于 ModelC 被允许不必明确说出它要做什么就可以继承这一行为。幸运的是,“为 derived classes(派生类)提供缺省的行为,但是除非它们提出明确的要求,否则就不交给他们”是很容易做到的。这个诀窍就是切断 virtual function(虚拟函数)的 interface(接口)和它的 default implementation(缺省实现)之间的联系。以下用的就是这个方法:
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination)
{
default code for flying an airplane to the given destination
}
注意 Airplane::fly 是被如何变成一个 pure virtual function(纯虚拟函数)的。它为飞行提供了 interface(接口)。那个缺省的实现也会出现在 Airplane class 中,但是现在它是一个独立的函数,defaultFly。像 ModelA 和 ModelB 这样需要使用缺省行为的 Classes 只是需要在他们的 fly 的函数体中做一下对 defaultFly 的 inline 调用(但是请参见 Item 30 提供的关于 inline 化和 virtual functions(虚拟函数)的交互作用的信息):
class ModelA: public Airplane {
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
...
};
class ModelB: public Airplane {
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
...
};
对于 ModelC class,不可能在无意中继承到不正确的 fly 的实现,因为 Airplane 中的 pure virtual(纯虚拟)强制要求 ModelC 提供的它自己的 fly 版本。
class ModelC: public Airplane {
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly(const Airport& destination)
{
code for flying a ModelC airplane to the given destination
}
这一方案并非十分安全(程序员还是能通过 copy-and-paste 使他们自己陷入麻烦),但是它比最初的设计更加可靠。至于 Airplane::defaultFly,它是 protected(保护的)是因为它完全是 Airplane 和它的 derived classes(派生类)的实现细节。使用飞机的客户应该只在意它能飞,而不必管飞行是如何实现的。
Airplane::defaultFly 是一个 non-virtual function(非虚拟函数)这一点也很重要。这是因为 derived class(派生类)不应该重定义这个函数,这是一个在 Item 36 中专门介绍的原则。如果 defaultFly 是 virtual(虚拟的),你就会遇到一个循环的问题:如果某些 derived class(派生类)应该重定义 defaultFly 却忘记了的时候会如何呢?
一些人反对为 interface(接口)和 default implementation(缺省实现)分别提供函数,就像上面的 fly 和 defaultFly 那样。首先,他们注意到,这样做会导致类似的相关函数名污染 class namespace(类名字空间)的问题。然而他们仍然同意 interface(接口)和 default implementation(缺省实现)应该被分开。他们是怎样解决这个表面上的矛盾呢?通过利用以下事实:pure virtual functions(纯虚拟函数)必须在 concrete derived classes(具体派生类)中被 redeclared(重声明),但是它们也可以有它们自己的实现。以下就是 Airplane hierarchy(继承体系)如何利用这一能力定义一个 pure virtual function(纯虚拟函数):
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
};
void Airplane::fly(const Airport& destination) // an implementation of
{ // a pure virtual function
default code for flying an airplane to
the given destination
}
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::fly(destination); }
...
};
class ModelC: public Airplane {
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly(const Airport& destination)
{
code for flying a ModelC airplane to the given destination
}
除了用 pure virtual function(纯虚拟函数)Airplane::fly 的函数体代替了独立函数 Airplane::defaultFly 之外,这是一个和前面的几乎完全相同的设计。本质上,fly 可以被拆成两个基本组件。它的 declaration(声明)指定了它的 interface(接口)(这是 derived classes(派生类)必须使用的),而它的 definition(定义)指定它的缺省行为(这是 derived classes(派生类)可以使用的,但只是在他们明确要求这一点时)。将 fly 和 defaultFly 合并,无论如何,你失去了给予这两个函数不同的保护层次的能力:原来是 protected 的代码(通过位于 defaultFly 中实现)现在成为 public(因为它位于 fly 中)。
最后,我们看看 Shape 的 non-virtual function(非虚拟函数),objectID:
class Shape {
public:
int objectID() const;
...
};
当一个 member function(成员函数)是 non-virtual(非虚拟的)时,不应该指望它在 derived classes(派生类)中的行为会有所不同。实际上,一个 non-virtual member function(非虚拟成员函数)指定了一个 invariant over specialization(超越特殊化的不变量),因为不论一个 derived class(派生类)变得多么特殊,它都把它看作是不允许变化的行为。如下所指除的,
你可以这样考虑 Shape::objectID 的声明,“每一个 Shape object 有一个产生 object identifier(对象标识码),而且这个 object identifier(对象标识码)总是用同样的方法计算出来的,这个方法是由 Shape::objectID 的定义决定的,而且 derived class(派生类)不应该试图改变它的做法。”因为一个 non-virtual function(非虚拟函数)被看作一个 invariant over specialization(超越特殊化的不变量),在 derived class(派生类)中他绝不应该被重定义,细节的讨论参见 Item 36。
对 pure virtual,simple virtual,和 non-virtual functions 的声明的不同允许你精确指定你需要 derived classes(派生类)继承什么东西。分别是 interface only(仅有接口),interface and a default implementation(接口和一个缺省的实现),和 interface and a mandatory implementation(接口和一个强制的实现)。因为这些不同的声明类型意味着根本不同的意义,当你声明你的 member functions(成员函数)时你必须在它们之间仔细地选择。如果你这样做了,你应该可以避免由缺乏经验的类设计者造成的两个最常见的错误。
第一个错误是声明所有的函数为 non-virtual(非虚拟)。这没有给 derived classes(派生类)的特殊化留出空间;non-virtual destructors(非虚拟析构函数)尤其有问题(参见 Item 7)。当然,完全有理由设计一个不作为 base class(基类)使用的类。在这种情况下,一套独享的 non-virtual member functions(非虚拟成员函数)是完全合理的。然而,更通常的情况下,这样的类既可能出于对 virtual(虚拟)和 non-virtual functions(非虚拟函数)之间区别的无知,也可能是对 virtual functions(虚拟函数)的性能成本毫无根据的担心的结果。事实是,几乎任何作为 base class(基类)使用的类都会有 virtual functions(虚拟函数)(还是参见 Item 7)。
如果你关心 virtual functions(虚拟函数)的成本,请允许我提起基于经验的 80-20 规则(参见 Item 30),在一个典型的程序中的情况是,80% 的运行时间花费在执行其中的 20% 的代码上。这个规则是很重要的,因为它意味着,平均下来,你的函数调用中的 80% 可以被虚拟化而不会对你的程序的整体性能产生哪怕是最轻微的可察觉的影响。在你走进对“你是否能负担得起一个 virtual function(虚拟函数)的成本”忧虑的阴影之前,应该使用一些简单的预防措施,以确保你关注的是你的程序中能产生决定性不同的那 20%。
另一个常见的错误声明所有的 member functions(成员函数)为 virtual(虚拟)。有时候这样做是正确的—— Item 31 的 Interface classes(接口类)可以作为证据。然而,它也可能是缺乏表明态度的决心的类设计者的标志。某些函数在 derived classes(派生类)中不应该被重定义,而且只要在这种情况下,你都应该通过将那些函数声明为 non-virtual(非虚拟)而明确地表达这一点。它不是为那些人服务的,他们假设如果他们只需花一些时间重定义你的所有函数,你的类就会被所有的人用来做所有的事情,如果你有一个 invariant over specialization(超越特殊化的不变量),请直说,不必害怕!
Things to Remember
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者