第15条:在资源管理类提供对原始资源的访问

来源:互联网 发布:java静态方法调用 编辑:程序博客网 时间:2024/06/05 00:24
资源管理类可有效防止程序发生资源泄漏。能否预防此类泄漏是一个系统设计方案是否优异的一个基本评判标准。可以完美依靠资源管理类来完成所有与资源间的互动,但永远也不能直接访问原始资源。然而也并不完美。许多 API 会直接引用资源,所以除非你不使用这样的 API (显然不切实际),否则,只能绕过资源管理器访问原始资源(raw resources)。举例说,第 13 条中引入了下面的做法 :使用诸如auto_ptr 或者 tr1::shared_ptr 这样的智能指针来保存诸如 createInvestment 这样的工厂函数的返回值:
std::tr1::shared_ptr<Investment> pInv(createInvestment());// 来自第 13 条

假设,当你使用一个Investment 对象时,你需要一个这样的函数:
int daysHeld(const Investment *pi);    // 返回投资持续的天数
你可能希望这样来调用它:
int days = daysHeld(pInv);     // 错!
但是这段代码无法通过编译:因为daysHeld 需要一个原始的 Investment* 指针,但是你传递给它的类型却是 tr1::shared_ptr<Investment> 对象。


你需要一个方法来将一个RAII 类对象(在上面的示例中是tr1::shared_ptr )转变为它所包含的原始资源(比如说,原始的 Investment* )。这里有两个方法可以完成:显式转换和隐式转换。tr1::shared_ptr  auto_ptr 都提供了一个 get 成员函数来进行显式转换,也就是说,返回一个智能指针对象中的裸指针(的副本):int days = daysHeld(pInv.get());       // 工作正常,将pInv 中的裸指针传递给daysHeld 
似乎所有的智能指针类,包括tr1::shared_ptr 和 auto_ptr 等等,都会重载指针解析运算符( operator-> 和 operator* ),他们允许隐式转换至底部的原始指针:
// 投资类型的层次结构中最为根基的部分class Investment {               public:  bool isTaxFree() const;  ...};Investment* createInvestment();    // 工厂函数std::tr1::shared_ptr<Investment> pi1(createInvestment());// 使用tr1::shared_ptr 管理资源bool taxable1 = !(pi1->isTaxFree());// 通过operator-> 访问资源...std::auto_ptr<Investment> pi2(createInvestment());// 使用 auto_ptr 管理资源bool taxable2 = !((*pi2).isTaxFree());// 通过 operator* 访问资源...


由于某些时刻你需要获取一个 RAII 对象中的原始资源,通过隐式转换。如下:
FontHandle getFont();    // 来自一个 C 语言 API// 省略参数表以简化代码void releaseFont(FontHandle fh);   // 来自同一个 C 语言 APIclass Font {     // RAII 类public:  explicit Font(FontHandle fh)     // 通过传值获取资源  : f(fh)       // 因为这一 C 语言 API 这样做  {}  ~Font() { releaseFont(f); }      // 释放资源private:  FontHandle f;    // 原始的字体资源};


假设有大量与字体相关的CAPI处理FontHandles,那么将Font 对象转换为FontHandle 的操作十分频繁。 Font 类可以提供一个显式转换函数,比如 get :
class Font {public:  ...  FontHandle get() const { return f; }// 进行显式转换的函数  ...};

遗憾的是这使得客户端程序员在每次使用 API 时就必须调用get :
void changeFontSize(FontHandle f, int newSize);// 来自一个 C 语言 APIFont f(getFont());int newFontSize;...changeFontSize(f.get(), newFontSize);  // 显式转换:从 Font 到 FontHandle


由于需要显式请求这样的转换,得不偿失,一些程序员也许会拒绝使用这个类。从而增加了字体资源泄漏的可能性,这与 Font 类的设计初衷是完全相悖的。
有一个替代方案, Font 提供一个将其隐式转换为 Fonthandle 的函数

class Font {public:  ...  operator FontHandle() const { return f; }                                 // 进行隐式转换的函数  ...};


这使得调用这一 C 语言 API 的工作变得简洁而且自然:
Font f(getFont());int newFontSize;...changeFontSize(f, newFontSize);    // 隐式转换:从 Font 到 FontHandle


但这个隐式转换会增加错误发生的机会。如一个客户端程序员在一个需要 Font 的地方意外地创建了一个 FontHandle :
Font f1(getFont());...FontHandle f2 = f1;   // 啊哦!本想复制一个Font 对象,但是却却将 f1 隐式转换为其原始的FontHandle,然后复制它

现在程序中有一个FontHandle 资源正在由 Font 对象 f1 来管理,但是仍然可以通过 f2 直接访问 FontHandle 资源。这是很糟糕的。比如说,当 f1 被销毁时,字体就会被释放, f2 也一样。
将RAII类转换为原始资源采用隐式或显式的方法,取决于 RAII 类被设计完成的具体任务,及其被使用的具体环境。最好的设计方案应该遵循第 18 条 的建议,让接口更容易被正确使用,不易被误用。通常情况下,定义一个类似于 get 的显式转换函数是一个较好的途径,应为它可以降低非故意类型转换的可能性。然而,使用隐式类型转换显得更加自然,人们更趋向于使用它。
可能让一个函数返回一个RAII 类内部的原始资源违背封装性原则的。但实际上并不那么糟糕。 RAII 类为了封装而存在。它的存在是为了确保资源释放的发生。如果需要,可以在这基本功能之上再加一层封装,但是这并不必要。另外,一 些 RAII 类 结合了实现封装的严格性和原始资源封装的宽松性。比如 tr1::shared_ptr 对其引用计数机制进行了整体封装,但它仍然为其所包含的裸指针提供了方便的访问方法。就像其它设计优秀的类一样,它隐藏了客户端程序员不需要关心的内容,但备妥需要的所有东西。
需要记住的1、API 通常需要访问原始资源,因此每个RAII 类都应该提供一个“取得它所管理的资源”的办法。2、对原始资源的访问可以经过显式或隐式转换。一般显式转换更安全,但隐式转换比较方便。 
0 0
原创粉丝点击