《Effective C++》Item18:让接口容易被正确使用,不易被误用

C++中充满了接口:function接口、class接口、template接口等,每一种接口都是客户与我们的代码互动的手段。假设我们面对的是一群“讲道理”的人,这部分客户希望能够正确地使用我们提供的借口。在这种情况下,如果它们对其中的任何一个接口的用法不正确,那么我们作为接口的设计者,也应该承担一些连带的责任。因为理论上,如果客户企图使用某个接口,但是却没有实现他所预期的行为,那么这段代码就不应该通过编译;反过来说,只要代码通过了编译,那么程序的行为就应该是用户所期望的那样。

想要开发一个“容易被正确使用、不容易被误用”的接口,首先要考虑用户在使用接口的时候可能会产生什么样的错误。假设现在我们设计了一套用于表现日期的类:

class Date 
{
public:
    Date(int month, int day, int year);
    //...
};

乍看之下,这个设计合情合理;但是客户却很容易犯下至少两个错误:

  • 他们可能会以错误的次序传递参数:
    Date d(30, 3, 1995); //但本意是:1995年3月30日。
    
  • 他们可能会传递一个非法的月份或者是天数:
    Date d(2, 30, 1995); //2月份不存在第30个自然日。
    

我们可以借助C++的类型系统来帮助客户避免这种错误的出现;也就是说,引入一组简单的包装类型,来区分天数、月份和年份,然后在Date的构造函数中使用这些类型:

class Day 
{
public: 
    explicit Day(int d) : val(d) {}
private:
    int val;
};

class Month 
{
public: 
    explicit Day(int m) : val(m) {}
private:
    int val;
};

class Year 
{
public: 
    explicit Day(int y) : val(y) {}
private:
    int val;
};

class Date
{
public:
    Date(const Month &m, const Day &d, const Year &y);
    //...
};

使用类似于DayMonthYear这种包装类,对预防“接口被误用”具有神奇的疗效。

一旦正确的类型准备完毕,那么同时也应该限制其取值。例如,一年只有12个有效的月份,所以Month应该反映出这个事实;其中一个方法是利用enum声明常量来表示月份,但是enum不具备我们希望拥有的那种类型安全性,例如enum可以被拿来当作一个整数来使用;比较安全的方法是预先定义处所有有效的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);
    //...
};

这里的方式是“使用函数替换对象,来表示某个特定月份”,遵循了条款4中的建议。

预防客户错误的另一个办法是,限制类型中可以进行的操作,常见的一种限制方式是遵循条款3的做法:添加尽可能多的const

下面是另一种设计良好接口的做法:尽可能地让自定义类型和原生数据类型的操作一致。这是因为客户非常熟悉基本数据类型有哪些行为,所以我们应该努力让自己的类型在合理的前提下具有相同的表现。例如,如果ab都是整数,那么对a*b的结果赋值并不合法;因此,我们也应该让自己的类型也具有相同的表现。

避免无端与内置类型不兼容,真正的理由是为了提供行为一致的接口。例如,STL中的接口十分一致,容器都有一个名为size的成员函数,它会告诉调用者目前容器内存在多少对象;与之形成对比的是Java的集合框架,它对原生数组使用属性length,对字符串String使用length()方法,而对列表List接口使用size()方法。

任何接口如果要求客户必须记得做某件事情,就是有着不正确使用的倾向,因为客户很可能会忘记做这件事情。例如条款13引入了一个工厂函数,返回一个指向Investment继承体系内的一个动态分配的对象:

Investment *createInvestment();

为了避免资源泄露,此工厂函数返回的指针最终必须被删除,但这一设定至少提供给了用户两次出错的机会:没有删除指针,或者删除同一个指针超过一次。

条款13介绍了客户如何将createInvestment的返回值存储于一个智能指针内,因而将delete的责任交给智能指针。但是万一客户忘记使用智能指针了怎么办?许多时候,较好的接口设计原则是先发制人,让工厂函数返回一个智能指针:

std::shared_ptr<Investment> createInvestment();

这便实质上强迫客户将返回值存储在一个shared_ptr中,消除了用户错误操作的可能性。

实际上,返回shared_ptr让接口设计者得以阻止一大群客户犯下资源泄露的错误。

假设类的设计者期望用户将该指针传递给名为getRidOfInvestment的函数来释放资源(而不是简单的通过delete)。现在这个接口又给了用户一个额外的犯错机会:用户可能会使用错误的资源析构机制。

同样,createInvestment的设计者可以先发制人:返回一个“将getRidOfInvestment”绑定为删除器的shared_ptr。这是因为shared_ptr有一个特别好的性质是,它允许每个指针定制自己的专属删除器。

其实这个特性非常棒,因为它会消除另外一个极难发现的错误:所谓的“跨DLL错误”。这个问题发生于“对象在动态链接库A中被构造,却在另一个动态链接库B中被删除”。在许多平台上,这一类“跨DLL的new/delete调用”会导致运行期错误;然而,使用了shared_ptr,就完全没有这个问题,因为它的删除器是来自对象诞生所在的那个DLL

诚然,在程序中使用智能指针,可能会损失一些性能,比方说它的体积往往是原生指针的两倍,还可能存在虚函数调用等;但是对于许多应用程序中,这些额外的执行成本并不显著,然而其“降低客户错误”的成效却是每个人都能够看得到的。

【注意】

  • 好的接口很容易被正确使用,不容易被误用。应该在所有的接口中努力达成这些性质。
  • “促进接口正确使用”的办法包括接口的一致性,以及与基本数据类型的行为相兼容。
  • “阻止接口误用”的方法包括建立新类型、限制类型上的操作、限制对象的值,以及消除客户的资源管理责任。
  • shared_ptr支持自定义删除器,这可以防范DLL问题,可被用来自动解除互斥量等等。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值