C++条款 让接口容易被正确使用,不易被误用 9/55

让接口容易被正确使用,不易被误用
Make interfaces easy to use correctly and hard to use incorrectly
若客户企图把事情做好,他们想要正确使用你的接口,这种情况下如果他们对任何其中一个接口的用法不正确,你至少也得负一部分责任。欲开发一个“容易被正确使用,不易被误用”的接口,首先必须考虑客户可能做出什么样的错误。假设你为一个用来表现日期的class设计构造函数:

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


初见这个接口通情达理,但它的客户很容易犯下至少两个错误。第一,他们也许会以错误的次序传递函数:

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

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

Date d(2, 30, 1995);    //2月无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(d)
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(3), Year(1995));     //OK,类型正确

令Day,Month和Year成为成熟且经充分锻炼的classes并封装其内数据,比简单使用上述的struct好。

一旦正确的类型已定位,限制其值有时候是通情达理的。例如一年只要12个月份,所以Month应该反映这一事实。办法之一是利用enum表现月份,但enum不具备我们希望拥有的类型安全性;比较安全的解法是预先定义所有有效的Months:

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));   

  下面是另一个一般性准则“让types容易被正确使用,不容易被误用”的表现形式:“除非有好理由,否则应该尽量让你的types的行为与内置types一致”。客户已经知道像Int有些什么行为,所以你应该努力让你的types在合理的前提下有相同的表现。

避免无端与内置类型不兼容,真正的理由是为了提供行为一致的接口。很少有其他性质比得上“一致性”更能导致“接口容易被正确使用”,也很少有其他性质比得上“不一致性”更加剧接口的恶化。

任何接口如果要求客户必须记得做某些事情,就是有着“不正确使用”的倾向,因为客户可能会忘记做那件事。

例如之前的条款导入了一个factory函数,它返回一个指针指向Investment继承体系内的一个动态分配对象:

Investment* createInvestment();

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

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

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

这便实质上强迫客户返回值存储于一个tr1::shared_ptr内,几乎消除了忘记删除底部Investment对象的可能性。

假设class设计者期许那些“从createInvestment取得Investment指针”的客户将该指针传递给一个名为getRidOfInvestment的函数,而不是直接使用delete。这样的一个接口又开启通往另一个客户犯错的大门,该错误是“企图使用错误的资源析构机制”(也就是拿delete替换getRidOfInvestment)。createInvestment的设计者可以针对此问题先发制人:返回一个“将getRidOfInvestment绑定为删除器”的tr1::shared_ptr。

tr1::shared_ptr提供的某个构造函数接受两个实参:一个是被管理的指针,另一个是引用次数变为0时将被调用的“删除器”。这启发我们创建一个Nulltr1::shared_ptr并以getRidOfInvestment作为其删除器:

std::tr1::shared_ptr<Investment>
    pInv(0, getRidOfInvestment);

但这并不是有效的C++。tr1::shared::ptr构造函数坚持其第一参数必须是个指针,而0不是指针,是个int。但它可以被转型为指针:

std::tr1::shared_ptr<Investment>
    pInv(static_cast<Investment*>(0), getRidOfInvestment);

如果我们要实现createInvestment使它返回一个tr1::shared_ptr并夹带getRidOfInvestment函数作为删除器,代码看起来是这样:

std::tr1::shared_ptr<Investment>createInvestment()
{
    std::tr1::shared_ptr<Investment>
        retVal(static_cast<Investment*>(0), getRidOfInvestment);
    retVal = ...;    //令retVal指向正确的的对象
    return retVal;
}

当然啦,如果被pInv管理的原始指针可以在建立pInv之前先确定下来,那么“将原始指针传给pInv构造函数”会比“先将pInv初始化为null再对它做一次赋值操作”为佳。

tr1::shared_ptr有一个特别好的性质是:它会自动使用它的“每个指针专属的删除器”,因而消除另一个潜在的客户错误:所谓的"cross-DLL problem"。这个问题发生于“对象在动态连接程序库(DLL)中被new创建,却在另一个DLL内被deletes销毁”。这一类“跨DLL的new/delete成对运用”会导致运行期错误。tr1::shared_ptr没有这个问题,因为它缺省的删除器是来自“tr1::shared_ptr诞生所在的那个DLL”的delete。例如:
如果Stock派生自Investment而createInvestment实现如下:

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

返回的那个tr1::shared_ptr可被传递给任何其他DLL,无需在意"cross-DLL problem"。这个指向Stock的tr1::shared_ptr会追踪记录“当Stock的引用次数变成0时可调用的那个DLL's delete”。

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值