Item 18:让接口容易被正确使用,不易被误用
正确地构造一个 Date
Date 对象的构造函数需要传入月、日、年。但客户在调用时常常传错顺序,这时可以将参数封装为对象来提供类型检查:
class Date{
public:
Date(const Month& m, const Day& d, const Year& y);
};
Date d(Day(30), Month(3), Year(1995)); // 编译错:类型不兼容!
Date d(Month(3), Day(30), Year(1995)); // OK
即使这样,用户的 Month 构造函数仍然会传入一个不合理的参数(例如 32),或者搞不清楚下标从0还是1开始。 解决方案是预定义所有可用的 Month:
class Month {
public:
static Month Jan() { return Month(1); }
static Month Feb() { return Month(2); }
...
static Month Dec() { return Month(12); }
...
private:
explicit Month(int m);
...
};
Date d(Month::Mar(), Day(30), Year(1995));
可以将运行时的数据转换为编译期的名称,可以将错误检查提前到编译期。以此解决参数顺序和范围的误用。
限制类型的操作
防止可能的客户错误的另一个方法是限制对一个类型能够做的事情。施加限制的一个普通方法就是加上 const。
例如,使 operator* 的返回类型具有 const 资格是如何能够防止客户对用户自定义类型犯下这样的错误:
if (a * b = c) ... // oops, meant to do a comparison!
除非你有很棒的理由,否则就让你的类型的行为与内建类型保持一致。例如,如果 a 和 b 是 int,给 a*b 赋值是非法的。所以除非有一个非常棒理由脱离这种表现,否则,对你的类型来说这样做也应该是非法的。
提供一致的接口
提供一致的接口也很重要。例如STL容器封装了互不兼容的基本数据类型,为STL算法提供了非常一致的接口。
好的接口不会要求用户去记住某些事情。比如Investment* createInvestment()
要求客户记住及时去销毁, 那么客户很可能忘记了去 delete 或 delete了多次。
解决方案便是返回一个智能指针而不是原始资源,而我们返回智能指针时就能指定 deleter 来自定义销毁动作:
shared_ptr<Investment> createInvestment(){
// 销毁一项投资时,需要做一些取消投资的业务,而不是简单地`delete`
return shared_ptr<Investment>(new Stock, getRidOfInvestment);
}
shared_ptr 带来的好处还不仅仅是移除了客户的责任,同时还解决了跨 DLL 动态内存管理的问题。 在 DLL 中new 的对象,如果在另一个 DLL 中 delete 往往会发生运行时错误,但 shared_ptr 进行资源销毁时, 总会调用创建智能指针的那个 DLL 中的 delete,这意味着 shared_ptr 可以随意地在 DLL 间传递而不需担心跨 DLL 的问题。
总结
- 好的接口易于正确使用,而难以错误使用。你应该在你的所有接口中为这个特性努力。
- 使易于正确使用的方法包括在接口和行为兼容性上与内建类型保持一致。
- 预防错误的方法包括创建新的类型,限定类型的操作,约束对象的值,以及消除客户的资源管理职责。
- shared_ptr 支持自定义 deleter。这可以防止 cross-DLL 问题,能用于自动解锁互斥体等。