博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
要让接口易于正确使用,而不易被误用
阅读量:2284 次
发布时间:2019-05-09

本文共 5471 字,大约阅读时间需要 18 分钟。

 
C++
中到处充满了接口。函数接口、类接口、模板接口,等等。每个接口都是实现客户端程序员与我们的代码相交互的一种手段。假设客户通情达理,他们的项目也十分优秀,他们便会十分看重我们的接口是否易于正确使用。这是千真万确的,如果他们误用了这些的接口中的任一个,那么我们也难推其咎。在理想状态下,如果客户端程序员尝试使用一个接口,但是没有达到预期的效果,那么代码则不应通过编译。反之,如果代码通过了编译,那么则必须要符合客户端程序员的需求。
 
开发中我们应做到让接口更易于正确使用而不易被误用,这需要我们考虑到客户端程序员会犯的各种错误。请参见下面的示例,假设编写一个表示日期时间的类的构造函数:
class Date {
public:
 Date(int month, int day, int year);
 ...
};
 
乍一看,这一接口设计得很合理,但是当客户端程序员面对这样的接口时,很容易犯下两种错误。第一,他们可能会使用错误的传参顺序:
Date d(30, 3, 1995); // 啊哦,应该是“
3, 30
”而不是“
30, 3
第二,他们可能会传进一个无效的月份或日期:
Date d(3, 40, 1995); // 啊哦,应该是“
3, 30
”而不是
“3, 40”
 
客户端程序员犯下的许多错误是可以通过引入新类型来避免的。实际上,对于防止不合要求的代码通过编译,类型系统是我们最得力的助手。在上述情况下,我们可以引入几个简单的“包装类型”来区别日期、月份、和年,然后再在
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
变得“羽翼丰满”,从而可以提供完善的数据封装。但是即使是结构体也足以说服我们:引入新的类型可以十分有效地防止接口误用的发生。
 
只要我们在恰当的地方使用了恰当的类型,便可以合理地限制这些类型的值。比如说,一年有
12
个月,所以
Month
类型应该能够反映出这一点。一个途径是使用枚举类型来表示月份,但是枚举类型并不总能达到我们对于安全的需求。比如说,枚举类型可以像
int
一样使用。一个更安全的解决方法是:预先定义好所有有效
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) ...    //本来是想进行一次比较!
实际,这仅是“让接口易于正确使用,而不易被误用”的另一种一般的做法:除非有更好的理由阻止我们这样做,否则应保证自己创建的类型的行为与内建数据类型保持一致。客户端程序员已经清楚
int
的行为,所以只要合情合理,就应该力求使我们的类拥有与
int
一致的行为。比如说,如
a
b
int
类型,那么为
a*b
值就是不合法的。
设计接口时应防止与内建数据类型发生不必要的不兼容性,这样做的真正目的是让各类接口拥有一致的行为。除了一致性,很少有特征可以让接口更加易于正确使用,同时,除了不一致性,也很少有特征可以让接口变得更加糟糕。
STL
容器的接口大体上(但并不完美)是一致的,这就使得它们更易于使用。比如说每个
STL
容器都有一个名为
size
成员函数,它可以告诉我们这一容器中容纳了多少对象。这一点与
Java
.NET
是不同的,
Java
中使用
length
属性
来表示数组的长度,
length
方法
来表示字符串的长度,以及
size
方法来表示
List
的大小。而
.NET
中的
Array
拥有一个叫做
Length
的属性,而
ArrayList
中功能相类似的属性则叫做
Count
。一些开发人员认为,集成开发环境(
IDE
)的存在让这类不一致性问题变得不那么重要,但是实际上他们大错特错了。不一致性问题会会给开发人员带来无穷尽的烦恼,而
IDE
是绝不能解决这些问题的。
 
任何接口都需要客户端程序员记忆一些易发生错误的内容,这是因为客户端程序员可能会把这些东西搞砸。比如说,引入一个工厂函数来返回一个指向
Investment
类中动态分配对象的指针:
Investment* createInvestment(); //为简化代码省略参数表
为防止资源泄漏,由
createInvestment
返回的指针在最后必须被删除,但是这将会给客户端程序员留下至少两个犯错误的机会:忘记删除指针、多于一次删除统一指针。
客户端程序员可以将
createInvestment
的返回值保存在诸如
auto_ptr
tr1::shared_ptr
这样的智能指针中,然后让智能指针担负起调用
delete
的责任。但是如果客户端程序员忘记了使用智能指针,这该怎么办呢?通常情况下,更好的接口的设计方案是:让工厂函数返回一个智能指针,在一开始就不给问题任何发生的机会。
std::tr1::shared_ptr<Investment> createInvestment();
这样便可以从根本上强制客户端程序员将返回值存储在一个
tr1::shared_ptr
中,在一个原始
Invesement
对象再有用之后,这样做可以防止忘记删除这一无用的对象。
事实上,返回
tr1::shared_ptr
让接口设计人员能够防止由资源释放所造成的客户端错误,这是因为
tr1::shared_ptr
允许在智能指针创建时,将资源释放函数(一个“删除器”)绑定在这一智能指针中,而
auto_ptr
没有这一功能。
 
假设一个客户端程序员从一个
createInvestment
中得到了一个
Investment*
指针,他可能会将这个指针传给一个名为
getRidOfInvestment
的函数,而不是将其删除。这样的接口将会为客户端程序员带来新的错误,客户端程序员将会使用错误的资源析构机制(也就是使用了
delete
而不是
getRidOfInvestment
)。实现
createInvestment
的程序员可以通过返回一个绑定了
getRidOfInvestment
“删除器”的
tr1::shared_ptr
来预防此类错误。
tr1::shared_ptr
提供了一个拥有两个参数的构造函数:需要管理的指针,以及当引用计数值为零时需要调用的删除器。关于创建绑定
getRidOfInvestment
“删除器”的
tr1::shared_ptr
,请看下面的方法:
std::tr1::shared_ptr<Investment> pInv(0, getRidOfInvestment);
// 尝试创建一个
null
shared_ptr,并且让其包含一个自定义的删除器;
// 这样的代码无法通过编译
 
这并不是合法的
C++
语法。
tr1::shared_ptr
的构造函数的第一个参数必须是一个指针,而
0
则不是,它是一个
int
值。的确,它可以转换成一个指针,但是这种情况下此类转换并不值得推荐,
tr1::shared_ptr
的第一个参数必须是一个实际的指针。通过一次转型可以解决这一问题:
std::tr1::shared_ptr<Investment>
 pInv(static_cast<Investment*>(0), getRidOfInvestment);
//创建一个
null
shared_ptr
,并且让其包含一个自定义的删除器;
上面的代码意味着,在实现
createInvestment
时,可让其返回一个“绑定了
getRidOfInvestment
删除器的
tr1::shared_ptr
”:
std::tr1::shared_ptr<Investment> createInvestment()
{
 std::tr1::shared_ptr<Investment>
      retVal(static_cast<Investment*>(0), getRidOfInvestment);
 retVal = ... ;    // 让
retVal
指向恰当的对象
 return retVal;
}
当然,如果在创建
pInv
之前就确定了其所管理的裸指针,那么将裸指针传递给
pInv
的构造函数更理想些,而不应该将
pInv
初始化为空值然后对其赋值。
 
使用
tr1::shared_ptr
的另一个较为显著的好处在于:它自动为每个指针预留一个删除器,用它们来排除另一类客户端错误,也就是所谓的“跨
DLL
问题”。这一问题在下面的情况中会发生:一个动态链接库(
DLL
)中使用
new
创建了一个对象,而这个对象在另一个
DLL
中由
delete
语句删除了。在许多平台上,此类跨
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
文件中自由穿梭,而不用考虑跨
DLL
问题。这一指向
Stock
tr1::shared_ptr
会始终追踪这一事件:当
Stock
的引用计数值为零时,需要使用哪一个
DLL
delete
语句。
 
这里主要讲如何让接口更加易于正确使用,而不易被误用,而不是
tr1::shared_ptr
,但是
tr1::shared_ptr
对于避免此类客户端错误却是一个不可多得的好工具,使用它是值得的。
tr1::shared_ptr
最为通用的实现来自
Boost
Boost
中的
shared_ptr
有两个裸指针那么大,它为记录和删除专用数据使用动态分配内存,当调用函数的删除器时使用虚函数,如果它认为一个应用程序是多线程的,那么当修改该程序的一个引用计数值时,将引入线程同步的开销。(也可以通过定义一个预处理记号来禁用多线程。)
Boost
中的
shared_ptr
也是有缺点的:它比裸指针更大,更慢,而且使用辅助的动态内存。在许多应用程序中,这些额外的运行时开销并不那么显著,但是它可以降低客户端程序员出错的可能,这一点对每个人来说都是十分显著的。
 
牢记在心
#
优秀的接口应该易于正确使用,而不易误用。对所有的接口都应该力争做到这一点。
#
保持与内置数据类型有一致的行为,是使接口易于正确使用的一种可行的方法
#
防止错误发生的方法有:创建新的数据类型,严格限定类型的操作,约束对象的值,不要将管理资源的任务留给客户端程序员。
#tr1::shared_ptr
支持自定义的删除功能。这可以防止
DLL
问题
,可以应用在自动解开互斥锁等情况下。
 

转载地址:http://vuznb.baihongyu.com/

你可能感兴趣的文章
tmpfs文件系统
查看>>
浏览器缓存
查看>>
favicon.ico引起的大量404
查看>>
Nginx缓存服务
查看>>
NFS一些问题
查看>>
利用TCP Wrappers构建sshd访问控制列表
查看>>
DenyHosts
查看>>
Maven构建环境安装
查看>>
SVN检出报错
查看>>
SVN同步版本库
查看>>
网络流量分析工具TCPDUMP
查看>>
系统弱密码检查John
查看>>
用户特权管理
查看>>
Linux软件包管理
查看>>
SUID和SGID可执行文件
查看>>
恢复已删除文件
查看>>
对敏感备份数据加密
查看>>
升级zlib
查看>>
扫描工具nmap
查看>>
linux Rootkit检查
查看>>