Item 18: 使 interfaces(接口)易于正确使用,而难以错误使用
作者:Scott Meyers
译者:fatalerror99 (iTePub's Nirvana)
发布:http://blog.csdn.net/fatalerror99/
C++ 被淹没于 interfaces(接口)中。function interfaces(函数接口)、class interfaces(类接口)、template interfaces(模板接口)。每一个 interface(接口)都是客户和你的代码交互的一种方法。如果你在和通情达理的人打交道,那些客户也想做好工作。他们想要正确使用你的 interfaces(接口)。在这种情况下,如果他们错误地使用了它,就说明你的 interface(接口)至少有部分是不完善的。在理想情况下,如果一个 interface(接口)的一种用法的尝试不符合客户的预期,代码将无法编译,反过来,如果代码可以编译,那么它做的就是客户想要的。
开发易于正确使用,而难以错误使用的 interfaces(接口)要求你考虑客户可能造成的各种错误。例如,假设你正在为一个代表日期的 class 设计 constructor(构造函数):
class Date {
public:
Date(int month, int day, int year);
...
};
乍一看,这个 interface(接口)似乎是合乎情理的(至少在美国),但是客户可能很容易地造成两种错误。首先,他们可能会以错误的顺序传递 parameters(参数):
Date d(30, 3, 1995); // Oops! Should be "3, 30" , not "30, 3"
第二,他们可能传递一个非法的月或日的数字:
Date d(3, 40, 1995); // Oops! Should be "3, 30" , not "3, 40"
(后面这个例子看上去好像很愚蠢,但是想想键盘上,4 就在 3 的旁边,这种 "off by one" 类型的错误并不罕见。)(上面这个例子原文有误,根据作者网站勘误更改——译者注。)
很多客户错误都可以通过新类型的引入来预防。确实,type system(类型系统)是你阻止那些不合理的代码通过编译的主要支持者。在当前情况下,我们可以引入简单的 wrapper types(包装类型)来区别日,月和年,并将这些类型用于 Data constructor(构造函数)。
struct Day { struct Month { struct Year {
explicit Day(int d) explicit Month(int m) explicit Year(int y)
:val(d) {} :val(m) {} :val(y){}
int val; int val; int val;
}; }; };
class Date {
public:
Date(const Month& m, const Day& d, const Year& y);
...
};
Date d(30, 3, 1995); // error! wrong types
Date d(Day(30), Month(3), Year(1995)); // error! wrong types
Date d(Month(3), Day(30), Year(1995)); // okay, types are correct
将 Day,Month 和 Year 做成可以封装数据的羽翼丰满的 classes 比上面的简单地使用 structs 更好(参见 Item 22),但是即使是 structs 也足够证明明智的新类型引入在阻止 interface(接口)的错误使用方面能工作得非常出色。
只要将正确的类型放在适当的位置,它往往能合理地限定那些类型的值。例如,月仅有 12 个合法值,所以 Month 类型应该反映这一点。做到这一点的一种方法是用一个 enum(枚举)来代表月,但是 enums(枚举)不像我们希望的那样是 type-safe(类型安全)的。例如,enums(枚举)能被作为 ints 使用(参见 Item 2)。一个更安全的解决方案是预先确定所有合法的 Months 的集合:
class Month {
public:
static Month Jan() { return Month(1); } // functions returning all valid
static Month Feb() { return Month(2); } // Month values; see below for
... // why these are functions, not
static Month Dec() { return Month(12); } // objects
... // other member functions
private:
explicit Month(int m); // prevent creation of new
// Month values
... // month-specific data
};
Date d(Month::Mar(), Day(30), Year(1995));
如果用 functions 代替 objects 来代表特定月的主意让你感到奇怪,那可能是因为你忘了 non-local static objects(非局部静态对象)的初始化的可靠性是值得怀疑的。Item 4 能唤起你的记忆。
防止可能的客户错误的另一个方法是限制对一个类型能够做的事情。施加限制的一个通常方法就是加上 const。例如,Item 3 阐释了使 operator* 的返回类型 const-qualifying(具备 const 资格)是如何能够防止客户对 user-defined types(用户定义类型)犯下这样的错误:
if (a * b = c) ... // oops, meant to do a comparison!
实际上,这仅仅是另一条使类型易于正确使用而难以错误使用的普遍方针的一种表现:除非你有很棒的理由,否则就让你的类型的行为与 built-in types(内建类型)保持一致。客户已经知道像 int 这样的类型有怎样的行为,所以你应该努力使你的类型的行为无论何时都同样合理。例如,如果 a 和 b 是 ints,给 a*b 赋值是非法的。所以除非有一个非常棒理由背离这种行为,否则,对你的类型来说这样做也应该是非法的。拿不定主意时,就像 ints 那样做。
避免和 built-in types(内建类型)毫无理由的 incompatibilities(不兼容性)的真正原因是为了提供行为一致的 interfaces(接口)。很少有特性比 consistency(一致性)更有可能导出易于正确使用的 interfaces(接口),也很少有特性比 inconsistency(不一致性)更有可能导出令人郁闷的 interfaces(接口)。STL containers(STL 容器)的 interfaces(接口)在很大程度上(虽然并不完美)是一致的,而这使得它们相当易于使用。例如,每一种 STL containers(STL 容器)都有一个名为 size 的 member function(成员函数)可以知道这个 container(容器)中有多少 objects。与此对比的是 Java,在那里你对 arrays(数组)使用 length property(属性),对 Strings 使用 length method(方法),而对 Lists 却要使用 size method(方法),在 .NET 中,Arrays 有一个名为 Length 的 property(属性),而 ArrayLists 却有一个名为 Count 的 property(属性)。一些开发人员认为 integrated development environments (IDEs)(集成开发环境)能补偿这些琐细的 inconsistencies(不一致),但他们错了。inconsistency(不一致)在开发者工作中强加的精神负担是任何 IDE 都无法完全消除的。
任何一个要求客户记住做某些事情的 interface(接口)都是倾向于错误使用的,因为客户可能忘记做那些事情。例如,Item 13 引入了一个 factory function(工厂函数),它返回一个指向动态分配的 Investment hierarchy(继承体系)中的 objects(对象)的指针。
Investment* createInvestment(); // from Item 13; parameters omitted
// for simplicity
为了避免 resource leaks(资源泄漏),从 createInvestment 返回的指针最后必须被删除,但这就为至少两种类型的客户错误创造了机会:没有删除指针,或删除同一个指针一次以上。
Item 13 展示了客户可以将 createInvestment 的返回值存入一个类似 auto_ptr 或 tr1::shared_ptr 的 smart pointer(智能指针)的方式,从而将使用 delete 的职责交给 smart pointer(智能指针)。但是如果客户忘记使用 smart pointer(智能指针)呢?在很多情况下,一个更好的 interface(接口)策略会通过让 factory function(工厂函数)首先返回一个 smart pointer(智能指针)而预先解决问题:
std::tr1::shared_ptr<Investment> createInvestment();
这就从根本上强制客户将返回值存入一个 tr1::shared_ptr,几乎完全消除了当底层的 Investment object 不再使用的时候却忘记删除的可能性。
实际上,返回一个 tr1::shared_ptr 使得一个 interface(接口)设计者预防许多其它的与资源泄漏释放相关的客户错误成为可能,因为,就像 Item 14 阐述的:当一个 smart pointer(智能指针)被创建的时候,tr1::shared_ptr 允许将一个 resource-release function(资源释放函数)——一个 "deleter" ——绑定到这个 smart pointer(智能指针)上。(auto_ptr 则没有这个能力。)
假设从 createInvestment 得到一个 Investment* 指针的客户期望将这个指针传给一个名为 getRidOfInvestment 的函数,而不是对它使用 delete。这样一个 interface(接口)又为一种新的客户错误打开了大门,这就是客户可能使用了错误的 resource-destruction mechanism(资源析构机制)(也就是说,用了 delete 而不是 getRidOfInvestment)。createInvestment 的实现通过返回一个在其 deleter 上绑定了 getRidOfInvestment 的 tr1::shared_ptr 能够预防这样的问题。
tr1::shared_ptr 提供了一个取得两个 arguments(实参)(要被管理的指针和当引用计数变为零时要调用的 deleter)的 constructor(构造函数)。这里展示了创建一个以 getRidOfInvestment 作为 deleter 的 null tr1::shared_ptr 的方法:
std::tr1::shared_ptr<Investment> // attempt to create a null
pInv(0, getRidOfInvestment); // shared_ptr with a custom deleter;
// this won't compile
唉呀,这不是合法的 C++。tr1::shared_ptr constructor(构造函数)坚决要求它的第一个参数是一个指针,而 0 不是一个指针,它是一个 int。当然,它 convertible(能转换)为一个指针,但那在当前情况下并不够好,tr1::shared_ptr 坚决要求一个真正的指针。用一个 cast(强制转型)解决个问题:
std::tr1::shared_ptr<Investment> // create a null shared_ptr with
pInv(static_cast<Investment*>(0), // getRidOfInvestment as its
getRidOfInvestment); // deleter; see Item 27 for info on
// static_cast
据此,实现返回一个以 getRidOfInvestment 作为其 deleter 的 tr1::shared_ptr 的 createInvestment 的代码看起来就像这个样子:
std::tr1::shared_ptr<Investment> createInvestment()
{
std::tr1::shared_ptr<Investment> retVal(static_cast<Investment*>(0),
getRidOfInvestment);
retVal = ... ; // make retVal point to the
// correct object
return retVal;
}
当然,如果将被 retVal 管理的 raw pointer(裸指针)可以在创建 retVal 时被确定,最好是将这个 raw pointer(裸指针)传给 retVal 的 constructor(构造函数),而不是将 retVal 初始化为 null 然后再赋值给它。至于方法上的细节,参考 Item 26。(此段原文有误,根据作者网站勘误更改——译者注。)
tr1::shared_ptr 的一个特别好的特性是它自动 per-pointer(逐指针)地使用 deleter 以消除另一种潜在的客户错误——“cross-DLL 问题。”这个问题发生在这种情况下:一个 object 在一个 dynamically linked library (DLL)(动态链接库)中通过 new 被创建,在另一个不同的 DLL 中被删除。在许多平台上,这样的 cross-DLL new/delete 对会导致运行时错误。tr1::shared_ptr 避免了这个问题,因为它的缺省的 deleter 只将 delete 用于这个 tr1::shared_ptr 被创建的同一个 DLL 中。这就意味着,例如,如果 Stock 是一个继承自 Investment 的 class,而且 createInvestment 被实现如下,
std::tr1::shared_ptr<Investment> createInvestment()
{
return std::tr1::shared_ptr<Investment>(new Stock);
}
返回的 tr1::shared_ptr 能在 DLLs 之间进行传递,而不必关心 cross-DLL 问题。指向这个 Stock 的 tr1::shared_ptrs 将保持对“当这个 Stock 的引用计数变为零的时候,哪一个 DLLs 的 delete 应该被使用”的跟踪。
这个 Item 不是关于 tr1::shared_ptr 的——而是关于使接口易于正确使用,而难以错误使用的——但 tr1::shared_ptr 正是这样一个消除某些客户错误的简单方法,它大体上值回了使用它的成本。最通用的 tr1::shared_ptr 实现来自于 Boost(参见 Item 55)。Boost 的 shared_ptr 的大小是一个 raw pointer(裸指针)的两倍,为簿记和 deleter-specific(deleter 专用)数据使用动态分配内存,当调用它的 deleter 时使用一个 virtual function(虚拟函数)来调用,在一个它认为是 multithreaded(多线程)的应用程序中,每当改变引用计数,会导致 thread synchronization(线程同步)开销。(你可以通过定义一个 preprocessor symbol(预处理符号)来使多线程支持失效。)在缺点方面,它比一个 raw pointer(裸指针)大,比一个 raw pointer(裸指针)慢,而且要使用辅助的动态内存。在许多应用程序中,这些附加的运行时开销并不显著,而对客户错误的减少却是每一个人都看得见的。
Things to Remember
- 好的 interfaces(接口)是易于正确使用,而难以错误使用的。你应该在你的所有 interfaces(接口)中为这个特性努力。
- 使易于正确使用的方法包括在 interfaces(接口)和 behavioral compatibility(行为兼容性)上与 built-in types(内建类型)保持一致。
- 预防错误的方法包括创建新的类型,限定类型的操作,约束 object 的值,以及消除客户的资源管理职责。
- tr1::shared_ptr 支持自定义 deleter。这可以防止 cross-DLL 问题,能用于自动解锁互斥体(参见 Item 14)等。