您的位置:首页 > 其它

条款18:让接口容易被正确使用,不易被误用

2014-05-29 18:45 417 查看
条款18:让接口容易被正确使用,不易被误用
开发易于正确使用,不容易被误用的接口需要你考虑客户可能造成的各种错误

如下设计一个日期类

class Date
{
public:
Date(const int month, const int day, int const year)
: m_month(month)
, m_day(day)
, m_year(year)
{

}
private:
int m_day;
int m_month;
int m_year;
};
错误调用

Date d1(29, 5, 2014);  //调用顺序错乱,应该是 5, 29, 2014
Date d2(2, 30, 2014);  //传入参数有误,2月没有30号
通过一种结构类型防止客户调用时发生类型错误

struct Day
{
explicit Day(const int day) : m_day(day) {}
private:
int m_day;
};

struct Month
{
explicit Month(const int month) : m_month(month) {}
private:
int m_month;
};

struct Year
{
explicit Year(const int year) : m_year(year) {}
private:
int m_year;
};

class Date
{
public:
Date(const Month &month, const Day &day,  const Year &year)
: m_month(month)
, m_day(day)
, m_year(year)
{

}
private:
Day m_day;
Month m_month;
Year m_year;
};
类型错误得到预防,但值还是没有得到保障

Date d2(2, 30, 2014);  //error,类型错误
Date d3(Day(30), Month(2), Year(2014)); //error,类型错误
Date d4(Month(2), Day(30), Year(2014)); //ok
可通过设计对应的类型的值限制来达到

struct Month
{
enum E_MON{JAN = 1, FEC, MAR, APR, MAY, JUN, JUL, AGU, SEP, OCT, NOV, DEC};
explicit Month(const E_MON month) : m_month(month) {}
private:
int m_month;
};
调用

Date d4(Month(Month::E_MON::DEC), Day(30), Year(2014)); //ok
防止可能的客户错误的另一个方法是限制对一个类型能够做的事情。施加限制的一个普通方法就是加上 const。在条款3中解释了使 operator* 的返回类型具有 const 资格是如何能够防止客户对用户自定义类型犯下这样的错误:

if (a * b = c) ...

这仅仅是另一条使类型易于正确使用而难以错误使用的普遍方针的一种表现:除非你有很棒的理由,否则就让你的类型的行为与内建类型保持一致。客户已经知道像 int 这样的类型如何表现,所以你应该努力使你的类型的表现无论何时都同样合理。例如,如果 a 和 b 是 int,给 a*b 赋值是非法的。所以除非有一个非常棒理由脱离这种表现,否则,对你的类型来说这样做也应该是非法的。

避免和内建类型毫无理由的不相容的真正原因是为了提供行为一致的接口。很少有特性比一致性更易于引出易于使用的接口,也很少有特性比不一致性更易于引出令人郁闷的接口。STL 容器的接口在很大程度上(虽然并不完美)是一致的,而且这使得它们相当易于使用。例如,每一种 STL 容器都有一个名为 size 的成员函数可以知道容器中有多少对象。

任何一个要求客户记住某些事情的接口都是有错误使用倾向的,因为客户可能忘记做那些事情。例如,Item 13 介绍了一个 factory 函数,它返回一个指向动态分配的 Investment 继承体系中的对象的指针。 假设从 createInvestment 得到一个 Investment* 指针的客户期望将这个指针传给一个名为 getRidOfInvestment 的函数,而不是对它使用 delete。这样一个接口又为一种新的客户错误打开了门,这就是客户可能使用了错误的资源析构机制(也就是说,用了
delete 而不是 getRidOfInvestment)。createInvestment 的实现可以通过返回一个在它的 deleter 上绑定了getRidOfInvestment 的 tr1::shared_ptr 来预防这个问题。
tr1::shared_ptr 提供了一个需要两个参数(要被管理的指针和当引用计数变为零时要调用的 deleter)的构造函数。

tr1::shared_ptr 的一个特别好的特性是它自动逐指针地使用 deleter 以消除另一种潜在的客户错误——“cross-DLL 问题。”这个问题发生在这种情况下:一个对象在一个动态链接库(dynamically linked library (DLL))中通过 new
被创建,在另一个不同的 DLL 中被删除。在许多平台上,这样的 cross-DLL new/delete 对会引起运行时错误。tr1::shared_ptr 可以避免这个问题,因为它的缺省的 deleter 只将 delete 用于这个 tr1::shared_ptr 被创建的 DLL 中。这就意味着,例如,如果 Stock 是一个继承自 Investment 的类,而且 createInvestment 被实现如下,

std::tr1::shared_ptr<Investment> createInvestment()
{
return std::tr1::shared_ptr<Investment>(new Stock);
}

返回的 tr1::shared_ptr 能在 DLL 之间进行传递,而不必关心 cross-DLL 问题。指向这个 Stock 的 tr1::shared_ptr 将保持对“当这个 Stock 的引用计数变为零的时候,哪一个 DLL 的 delete 应该被使用”的跟踪。

这个 Item 不是关于 tr1::shared_ptr 的——而是关于使接口易于正确使用,而难以错误使用的——但 tr1::shared_ptr 正是这样一个消除某些客户错误的简单方法,值得用一个概述来看看使用它的代价。

记住
①好的接口易于正确使用,而难以错误使用。你应该在你的所有接口中为这个特性努力。

②使易于正确使用的方法包括在接口和行为兼容性上与内建类型保持一致。

③预防错误的方法包括创建新的类型,限定类型的操作,约束对象的值,以及消除客户的资源管理职责。

④tr1::shared_ptr 支持自定义 deleter。这可以防止 cross-DLL 问题,能用于自动解锁互斥体(参见 Item 14)等。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: