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

        欲开发一个“容易被正确使用,不容易被误用”的接口,首先必须考虑客户可能做出什么样的错误。假设你为一个用来表现日期的class设计构造函数:

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

        乍见之下这个接口通情达理(至少在美国如此),但它的客户很容易犯下至少两个错误。

第一,他们也许会以错误的次序传递参数:

Date(30, 3, 1995);  // 应该是“3, 30”

第二,他们可能传递一个无效的月份或者天数:

Date(2, 30, 1995);  // 应该是“3, 30”

        许多客户端错误可以因为误入新类型而获得预防。在防范“不值得拥有的代码”上,类型系统(type system)是你的主要同盟国。既然这样,就让我们导入简单的外覆类型(wrapper types)来区别天数、月份和年份,然后于Date构造函数中使用这些类型:

struct Day {
	explicit Day(int d)
	: val(d) {}
	int val;
};

struct Month {
	explicit Month(int m)
	: val(m) {}
	int val;
};

struct Year {
	explicit Year(int y)
	: val(y) {}
	int val;
};

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

Date d(30, 3, 1995);  // 错误!不正确的类型
Date d(Day(30), Month(3), Year(1995)); // 错误!不正确的类型
Date d(Month(3), Day(30), Year(1995)); // OK,类型正确

        令Day,Month和Year成为成熟且经充分锻炼的classes并封装其内数据,比简单使用上述的structs好(见条款22)。但即使structs也已经足够示范:明智而审慎地导入新类型对预防“接口被误用”有神奇疗效。

        一旦正确的类型定位,限制其值有时候是通情达理的。例如一年只有12个有效月份,所以Month应该反映这一事实。办法之一是利用enum表现月份,但enums不具备我们希望拥有的类型安全性,例如enums可被拿来当一个ints使用(见条款2)。

比较安全的解法是预先定义所有有效的Months:

class Month {
	public:
	static Month Jan() {return Month(1);}  // 函数,返回有效月份。
	static Month Feb() {return Month(2);}  // 稍后解释为什么。
	...                                    // 这些是函数而非对象。
	static Month Dec() {return Month(12);} 
	...
	private:
	explict Month(int m);  // 阻止生成新的月份。
	...                    // 这是月份专属数据。
};
Date d(Month::Mar(), Day(30), Year(1995));

        如果“以函数替换对象,表现某个特定月份”让你觉得诡异,或许是因为你忘记了non-local static对象的初始化次序有可能出问题。建议阅读条款4恢复记忆。

        上述处理方式,目前看来太过繁琐,通常项目开发中,我们可以使用断言来判定接口参数是否合法。主要是要理解上述内容的思想。可以使用新类型,确保提供给用户的接口方便使用且不易出错。

        预防客户错误的另一个办法是,限制类型内什么事可做,什么事不能做。常见的限制是加上const。例如条款3曾经说明为什么“以const修饰operator*的返回类型”可阻止客户因“用户自定义类型”而犯错:

if (a * b = c) ...  // 原意其实是要做一次比较动作!

        下面另一个一般性准则“让types容易被正确使用,不容易被误用”的表现形式:“除非有好理由,否则应该尽量令你的types的行为与内置types一致”。客户已经知道像int这样的type有些什么行为,所以你应该努力让你的types在合样合理的前提下也有相同表现。例如,如果a和b都是ints,那么对a*b赋值并不合法。

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

Investment* createInvestment();  // 来自条款13;为求简化暂略参数。

        为避免资源泄漏,createInvestment返回的指针最终必须被删除,但那至少开启了两个客户错误机会:没有删除指针,或删除同一个指针超过一次。

        条款13表明客户如何将createInvestment的返回值存储于一个智能指针如auto_ptr或tr1::shared_ptr内,因而将delete责任推个智能指针。但万一客户忘记使用智能指针怎么办?许多时候,较佳接口的设计原则是先发制人,就令factory函数返回一个智能指针:

std::tr1::shared_ptr<Investment> createInvestment();

        这便实质上强迫客户将返回值存储于一个tr1::shared_ptr内,几乎消弭了忘记删除底部Investment对象(当它不再被使用时)的可能性。

        实际上,返回tr1::shared_ptr让接口设计者得以阻止一大群客户犯下资源泄漏的错误,因为就如条款14所言,tr1::shared_ptr允许当智能指针被建立起来时指定一个资源释放函数(所谓删除器,“deleter”)绑定于智能指针身上(auto_ptr就没有这种能耐)。

        本条款并非特别针对tr1::shared_ptr,而是为了“让接口容易被正确使用,不容易被误用”而设。但由于tr1::shared_ptr如此容易消除某些客户错误,值得我们核计其使用成本。最常见的tr1::shared_ptr实现来自Boost(见条款55)。Boost的shared_ptr是原始指针(raw pointer)的两倍大,以动态分配内存作为薄记用途和“删除器之专属数据”,以virtual形式调用删除器,并在多线程程序修改引用次数时蒙受线程同步化的额外开销。总之,它比原始指针大且慢,而且使用辅助动态内存。在许多应用程序中这些额外的执行成本并不显著,然而其“降低客户错误”的成效却是每个人都看得到。

请记住

  • 好的接口容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
  • “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
  • “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
  • tr1::shared_ptr支持定制型删除器(custom deleter)。这可防范DLL问题,可被用来自动解除互斥锁(mutexes;见条款14)等等。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值