C 箴言:使接口易于正确使用

2008-02-23 05:40:40来源:互联网 阅读 ()

新老客户大回馈,云服务器低至5折

  C 被淹没于接口中。函数接口、类接口、模板接口。每一个接口都意味着客户的代码和您的代码互相影响。假设您在和通情达理的人打交道,那些客户也想做好工作。他们想要正确使用您的接口。在这种情况下,假如他们犯了一个错误,就说明您的接口至少有部分是不完善的。在理想情况下,假如一个接口的一种尝试的用法不符合客户的预期,代码将无法编译,反过来,假如代码能够编译,那么他做的就是客户想要的。

  研发易于正确使用,而难以错误使用的接口需要您考虑客户可能造成的各种错误。例如,假设您正在设计一个代表时间的类的构造函数:

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

  匆匆一看,这个接口似乎是合乎情理的(至少在美国),但是客户可能很容易地造成两种错误。首先,他们可能会以错误的顺序传递参数:

Date d(30, 3, 1995); // Oops! Should be "3, 30" , not "30, 3"

  第二,他们可能传递一个非法的代表月或日的数字:

Date d(2, 20, 1995); // Oops! Should be "3, 30" , not "2, 20"

  (后面这个例子看上去似乎没什么,但是想想键盘上,2 就在 3 的旁边,这种 "off by one" 类型的错误并不罕见。)

  很多客户错误都能够通过引入新的类型来预防。确实,类型系统是您阻止那些不合适的代码通过编译的主要支持者。在当前情况下,我们能够引入简单的包装类型来区别日,月和年,并将这些类型用于 Data 的构造函数。

struct Day { struct Month { struct Year {
explicit Day(int d) explicit Month(int m) explicit Year(int y)
:val(d) {} :val(m) {} :val(y){}

int val; int val; int val;
}; }; };


class Date {
public:
Date(const Month& m, const Day& d, const Year& y);
...
};
Date d(30, 3, 1995); // error! wrong types

Date d(Day(30), Month(3), Year(1995)); // error! wrong types

Date d(Month(3), Day(30), Year(1995)); // okay, types are correct

  将日,月和年做成封装数据的羽翼丰满的类比上面的简单地使用 struct 更好,但是即使是 struct 也足够证实明智地引入新类型在阻止接口的错误使用方面能工作得很出色。

  只要放置了正确的类型,他往往能合理地限制那些类型的值。例如,月仅有 12 个合法值,所以 Month 类型应该反映这一点。做到这一点的一种方法是用一个枚举来表现月,但是枚举不像我们希望的那样是类型安全(type-safe)的。例如,枚举能被作为整数使用。一个安全的解决方案是预先确定合法的 Month 的集合:

class Month {
public:
static Month Jan() { return Month(1); } // functions returning all valid
static Month Feb() { return Month(2); } // Month values; see below for
... // why these are functions, not
static Month Dec() { return Month(12); } // objects

... // other member functions

private:
explicit Month(int m); // prevent creation of new
// Month values

... // month-specific data
};
Date d(Month::Mar(), Day(30), Year(1995));

  假如用函数代替对象来表现月的主意让您感到惊奇,那可能是因为您忘了非局部静态对象(non-local static objects)的初始化的可靠性是值得怀疑的。Item 4 能唤起您的记忆。

  防止可能的客户错误的另一个方法是限制对一个类型能够做的事情。施加限制的一个普通方法就是加上 const。例如,Item 3 解释了使 operator* 的返回类型具备 const 资格是如何能够防止客户对用户自定义类型犯下这样的错误:

if (a * b = c) ... // oops, meant to do a comparison!

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

  避免和内建类型毫无理由的不相容的真正原因是为了提供行为一致的接口。很少有特性比一致性更易于引出易于使用的接口,也很少有特性比不一致性更易于引出令人郁闷的接口。STL 容器的接口在很大程度上(虽然并不完美)是一致的,而且这使得他们相当易于使用。例如,每一种 STL 容器都有一个名为 size 的成员函数能够知道容器中有多少对象。和此对比的是 Java,在那里您对数组使用 length 属性,对 String 使用 length 方法,而对 List 却要使用 size 方法,在 .NET 中,Array 有一个名为 Length 的属性,而 ArrayList 却有一个名为 Count 的属性。一些研发人员认为集成研发环境(IDEs)能补偿这些琐细的矛盾,但他们错了。矛盾在研发者工作中强加的精神折磨是任何 IDE 都无法完全消除的。

  任何一个需要客户记住某些事情的接口都是有错误使用倾向的,因为客户可能忘记做那些事情。例如,Item 13 介绍了一个 factory 函数,他返回一个指向动态分配的 Investment 继承体系中的对象的指针。

Investment* createInvestment(); // from Item 13; parameters omitted
// for simplicity

  为了避免资源泄漏,createInvestment 返回的指针最后必须被删除,但这就为至少两种类型的客户错误创造了机会:删除指针失败,或删除同一个指针一次以上。

  我在前面展示了客户能够怎样将 createInvestment 的返回值存入一个类似 auto_ptr 或 tr1::shared_ptr 智能指针,从而将使用 delete 的职责交给智能指针。但是假如客户忘记使用智能指针呢?在很多情况下,一个更好的接口会预先判定将要出现的问题,从而让 factory 函数在第一现场即返回一个智能指针:

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇: C 箴言:用传引用给const取代传值

下一篇: C 箴言:将new出来的对象存入智能指针