Item 28: 避免返回 object 内部构件的 "handles"(“句柄”)
作者:Scott Meyers
译者:fatalerror99 (iTePub's Nirvana)
发布:http://blog.csdn.net/fatalerror99/
假设你正在一个与矩形有关的应用程序上工作。每一个矩形都可以用它的左上角和右下角表示出来。为了将一个 Rectangle object 保持在较小状态,你可能决定定义 Rectangle 区域的那些点不应该存储在 Rectangle 自身之中,更合适的做法是放在一个由 Rectangle 指向的辅助的结构体中:
class Point { // class for representing points
public:
Point(int x, int y);
...
void setX(int newVal);
void setY(int newVal);
...
};
struct RectData { // Point data for a Rectangle
Point ulhc; // ulhc = " upper left-hand corner"
Point lrhc; // lrhc = " lower right-hand corner"
};
class Rectangle {
...
private:
std::tr1::shared_ptr<RectData> pData; // see Item 13 for info on
}; // tr1::shared_ptr
由于 Rectangle 的客户需要能操控一个 Rectangle 的区域,因此这个 class 提供了 upperLeft 和 lowerRight 函数。然而,Point 是一个 user-defined type(用户定义类型),所以,留心 Item 20 的关于在典型情况下,以传引用的方式传递 user-defined type(用户定义类型)比传值的方式更加高效的观点,这些函数返回引向底层 Point objects 的引用:
class Rectangle {
public:
...
Point& upperLeft() const { return pData->ulhc; }
Point& lowerRight() const { return pData->lrhc; }
...
};
这个设计可以编译,但它是错误的。实际上,它是自相矛盾的。一方面,upperLeft 和 lowerRight 被声明为 const member functions(成员函数),因为它们被设计成仅仅给客户提供一个获得 Rectangle 的点的方法,而不允许客户改变这个 Rectangle(参见 Item 3)。另一方面,两个函数都返回引向 private internal data(私有内部数据)的引用——调用者可以用来修改那些 internal data(内部数据)的引用!例如:
Point coord1(0, 0);
Point coord2(100, 100);
const Rectangle rec(coord1, coord2); // rec is a const rectangle from
// (0, 0) to (100, 100)
rec.upperLeft().setX(50); // now rec goes from
// (50, 0) to (100, 100)!
请注意这里,upperLeft 的调用者是如何能够利用返回的引向 rec 内部 Point data members(数据成员)的引用来改变这个成员的。但是 rec 却被假设为 const!
这直接引出两条教训。第一,一个 data member(数据成员)只能被封装到具有最高可访问级别的函数能返回一个引向它的引用的程度。在当前情况下,虽然 ulhc 和 lrhc 被它们的 Rectangle 认为是 private(私有)的(此处原文有误,根据作者网站勘误修改——译者注),但它们还是被有效地公开了,因为 public 函数 upperLeft 和 lowerRight 返回了引向它们的引用。第二,如果一个 const member function(成员函数)返回一个引用,引向一个与某个 object 有关并存储在这个 object 自身之外的数据,这个函数的调用者就可以改变那个数据(这正是 bitwise constness(二进制位常量性)的局限性(参见 Item 3)的一个副作用)。
我们已做的每件事都涉及到返回引用的 member functions(成员函数),但是,如果它们返回指针或者迭代器,因为同样的原因也会存在同样的问题。引用,指针,和迭代器都是 handles(句柄)(持有其它 objects 的方法),而返回一个 object 的 internals(内部构件)的 handle(句柄)总是面临危及 object 的 encapsulation(封装)的风险。就像我们看到的,它同时还能导致允许 const member functions(成员函数)改变一个 object 的状态。
我们通常认为一个 object 的 "internals"(“内部构件”)就是它的 data members(数据成员),但是不能被普通公众访问的 data members(成员函数)(也就是说,它是 protected 的或 private 的)也是 object 的 internals(内部构件)的一部分。同样,不要返回它们的 handles(句柄)也很重要。这就意味着你绝不应该有一个 member function(成员函数)返回一个指向拥有较小的可访问级别的 member function(成员函数)的指针。如果你这样做了,它的有效可访问级别就会与那个拥有较大的可访问级别的函数相同,因为客户能够得到指向这个拥有较小的可访问级别的函数的指针,然后就可以通过这个指针调用这个函数。
然而,返回指向 member functions(成员函数)的指针的函数是难得一见的,所以让我们把注意力返回到 Rectangle class 和它的 upperLeft 和 lowerRight member functions(成员函数)上。我们在这些函数中挑出来的问题都只需简单地将 const 用于它们的返回类型就可以消除:
class Rectangle {
public:
...
const Point& upperLeft() const { return pData->ulhc; }
const Point& lowerRight() const { return pData->lrhc; }
...
};
通过这个修改过的设计,客户可以读取定义一个矩形的 Points,但他们不能写它们。这就意味着将 upperLeft 和 lowerRight (此处原文有误,根据作者网站勘误修改——译者注)声明为 const 不再是一句空话,因为它们不再允许调用者改变 object 的状态。至于 encapsulation(封装)的问题,我们总是故意让客户看到组成一个 Rectangle 的 Points,所以这是 encapsulation(封装)的一个故意的放松之处。更重要的,它是一个 limited(有限)的放松:只有读访问是被这些函数准许的,写访问依然被禁止。
虽然如此,upperLeft 和 lowerRight 仍然返回一个 object 的 internals(内部构件)的 handles(句柄),而这在其它方面可能是有问题的。特别是,这会导致 dangling handles(空悬句柄):引用了不再存在的 objects 的构件的 handles(句柄)。这种消失的 objects 的最常规的来源就是函数返回值。例如,考虑一个函数,以一个矩形的形式返回一个 GUI object 的 bounding box:
class GUIObject { ... };
const Rectangle // returns a rectangle by
boundingBox(const GUIObject& obj); // value; see Item 3 for why
// return type is const
现在,考虑客户可能会如何使用这个函数:
GUIObject *pgo; // make pgo point to
... // some GUIObject
const Point *pUpperLeft = // get a ptr to the upper
&(boundingBox(*pgo).upperLeft()); // left point of its
// bounding box
对 boundingBox 的调用会返回一个新建的临时的 Rectangle object。这个 object 没有名字,所以让我们就称它为 temp。于是 upperLeft 就在 temp 上被调用,这个调用返回一个引向 temp 的一个内部构件的引用,特别是,它是由 Points 构成的。随后 pUpperLeft 指向这个 Point object。到此为止,一切正常,但是我们无法继续了,因为在这个语句的末尾,boundingBox 的返回值—— temp ——被销毁了,这将间接导致 temp 的 Points 的析构。接下来,剩下 pUpperLeft 指向一个已经不再存在的 object;pUpperLeft 空悬在创建它的语句的末尾!
这就是为什么任何一个返回一个 object 的内部构件的 handle(句柄)的函数都是危险的。这与那个 handle(句柄)是指针,引用,还是迭代器没什么关系。它与是否受到 cosnt 的限制没什么关系。这与那个 member function(成员函数)返回的 handle(句柄)本身是否是 const 没什么关系。全部的问题在于一个 handle(句柄)被返回了,因为一旦这样做了,你就面临着这个 handle(句柄)比它引用的 object 更长寿的风险。
这并不意味着你永远不应该让一个 member function(成员函数)返回一个 handle(句柄)。有时你必须如此。例如,operator[] 允许你从 strings 和 vectors 中抽出单独的元素,而这些 operator[]s 就是通过返回引向 containers(容器)中的数据的引用来工作的(参见 Itme 3)——当 containers(容器)本身被销毁,数据也将销毁。尽管如此,这样的函数属于特例,而不是惯例。
Things to Remember
- 避免返回 object internals(对象内部构件)的 handles(句柄)(引用,指针,或迭代器)。这样会提高 encapsulation(封装),帮助 const member functions(成员函数)产生 cosnt 效果,并将最小化 dangling handles(空悬句柄)产生的可能性。