Effective C# 原则18:实现标准的处理(Dispose)模式

简介:

 我们已经讨论过,处理一个占用了非托管资源对象是很重要的。现在是时候 来讨论如何写代码来管理这些类占用的非内存资源了。一个标准的模式就是利用 .Net框架提供的方法处理非内存资源。你的用户也希望你遵守这个标准的模式。也就是通过实现IDisposable接口来释放非托管的资源,当然是在用户记得调用 它的时候,但如果用户忘记了,析构函数也会被动的执行。它是和垃圾回收器一 起工作的,确保在一些必要时候,你的对象只会受到因析构函数而造成的性能损 失。这正是管理非托管资源的好方法,因此有必要彻底的弄明白它。

处 在类继承关系中顶层的基类应该实现IDisposable接口来释放资源。这个类型也 应该添加一个析构函数,做为最后的被动机制。这两个方法都应该是用虚方法来 释放资源,这样可以让它的派生类重载这个函数来释放它们自己的资源。派生类 只有在它自己须要释放资源时才重载这个函数,并且一定要记得调用基类的方法 。

开始时,如果你的类使用了非内存资源,则一定得有一个析构函数。 你不能指望你的用户总是记得调用Dispose方法,否则当他们忘记时,你会丢失 一些资源。这或许是因为他们没有调用Dispose的错误,但你也有责任。唯一可 以确保非内存资源可以恰当释放的方法就是创建一个析构函数。所以,添加一个 析构函数吧!

当垃圾回收器运行时,它会直接从内存中移除不用析构的垃 圾对象。而其它有析构函数的对象还保留在内存中。这些对象被添加到一个析构 队列中,垃圾回收器会起动一个线程专门来析构这些对象。当析构线程完成它的 工作后,这些垃圾对象就可以从内存中移除了。就是说,须要析构的对象比不须 要析构的对象在内存中待的时间要长。但你没得选择。如果你是采用的这种被动 模式,当你的类型占用非托管资源时,你就必须写一个析构函数。但目前你还不 用担心性能问题,下一步就保证你的用户使用更加简单,而且可以避免因为析构 函数而造成的性能损失。

实现IDisposable接口是一个标准的模式来告诉 用户和进行时系统:你的对象占有资源而且必须及时的释放。IDisposable接口 只有一个方法:

public interface IDisposable
{
void Dispose( );
}

实现IDisposable.Dispose()方法有责 任完成下面的任务:

1、感知所有的非托管资源。

2、感知所有的 托管资源(包括卸载一些事件)。

3、设置一个安全的标记来标识对象已经 被处理。如果在已经处理过的对象上调用任何方法时,你可以检验这个标记并且 抛出一个ObjectDisposed的异常。

4、阻止析构。你要调用 GC.SuppressFinalize(this)来完成最后的工作。

通过实现IDisposable 接口,你写成了两件事:第一就是提供了一个机制来及时的释放所有占用的托管 资源(译注:这里就是指托管资源,当实现了这个接口后,可以通过调用Dispose 来立即释放托管资源),另一个就是你提供了一个标准的模式让用户来释放非托 管资源。这是十分重要的,当你在你的类型上实现了IDisposable接口以后,用 户就可以避免析构时的损失。你的类就成了.Net社区中表现相当良好的成员。

但在你创建的机制中还是存在一些漏洞。如何让一个派生类清理自己的 资源,同时还可以让基类很好的再做资源清理呢?(译注:因为调用Dispose方法 时,必须调用基类的Dispose,当然是在基类有这个方法时。但前面说过,我们 只有一个标记来标识对象是否处理过,不管先调用那个,总得有一个方法不能处 理这个标记,而这就存在隐患) 如果基类重载了析构函数,或者自己添加实现了 IDisposable接口,而这些方法又都是必须调用基类的方法的;否则,基类无法 恰当的释放资源。同样,析构和处理共享了一些相同的职责:几乎可以肯定你是 复制了析构方法和处理方法之间的代码。正如你会在原则26中学到的,重载接口 的方法根本没有如你所期望的那样工作。Dispose标准模式中的第三个方法,通 过一个受保护的辅助性虚函数,制造出它们的常规任务并且挂接到派生类来释放 资源。基类包含接口的核心代码, 派生类提供的Dispose()虚函数或者析构函数 来负责清理资源:

protected virtual void Dispose( bool isDisposing );

重载的方法同时完成析构和处理必须提供的任务 ,又因为它是虚函数,它为所有的派生类提供函数入口点。派生类可以重载这个 函数,提供恰当的实现来释放它自己的资源,并且调用基类的函数。当

isDisposing为true时你可能同时清理托管资源和非托管资源,当 isDisposing为false时你只能清理非托管资源。两种情况下,都可以调用基类的 Dispose(bool)方法让它去清理它自己的资源。

当你实现这样的模式时, 这里有一个简单的例子。MyResourceHog 类展示了IDisposable的实现,一个析 构函数,并且创建了一个虚的Dispose方法:

public class MyResourceHog : IDisposable
{
// Flag for already disposed
private bool _alreadyDisposed = false;
// finalizer:
// Call the virtual Dispose method.
~MyResourceHog()
{
Dispose( false );
}
// Implementation of IDisposable.
// Call the virtual Dispose method.
// Suppress Finalization.
public void Dispose() 
{
Dispose( true );
GC.SuppressFinalize( true );
}
// Virtual Dispose method
protected virtual void Dispose( bool isDisposing )
{
// Don't dispose more than once.
if ( _alreadyDisposed )
return;
if ( isDisposing )
{
// TODO: free managed resources here.
}
// TODO: free unmanaged resources here.
// Set disposed flag:
_alreadyDisposed = true;
}
}

如果派生类有另外 的清理任务,就让它实现Dispose方法:

public class DerivedResourceHog : MyResourceHog
{
// Have its own disposed flag.
private bool _disposed = false;
protected override void Dispose( bool isDisposing )
{
// Don't dispose more than once.
if ( _disposed )
return;
if ( isDisposing )
{
// TODO: free managed resources here.
}
// TODO: free unmanaged resources here.
// Let the base class free its resources.
// Base class is responsible for calling
// GC.SuppressFinalize( )
base.Dispose( isDisposing );
// Set derived class disposed flag:
_disposed = true;
}
}

注和意,派生类和基类都有一个处理状态的标记, 这完全是被动的。重制的标记掩盖了在处理时任何可能发生的错误,而且是单一 的类型处理,而不是处理构成这个对象的所有类型。(译注:就是基类与子类各 自标记一个,互不影响。)

你应该被动的写处理方法和析构函数,处理对 象可能以任何顺序发生,你可能会遇到这种情况:你的类中某个成员在你调用 Dispose方法以前已经被处理过了。你没有看到这种情况是因为Dispose()方法是 可以多次调用的。如果在一个已经被处理过的对象上调用该方法,就什么也不发 生。析构函数也有同样的规则。任何对象的引用存在于内存中时,你不用检测 null引用。然而,你引用的对象可能已经处理掉了,或者它已经析构了。

这就引入用了一个非常重要的忠告:对于任何与处理和资源清理相关的 方法,你必须只释放资源! 不要在处理过程中添加其它任何的任务。你在处理和 清理中添加其它任务时,可能会在对象的生存期中遇到一些严重而繁杂的问题。 对象在你创建它时出生,在垃圾回收器认领它时死亡。你可以认为当你的程序不 能再访问它们时,它们是睡眠的。你无法访问对象,无法调用对象的方法。种种 迹象表明,它们就像是死的。但对象在宣布死亡前,析构函数还有最后一气。析 构函数什么也不应该做,就是清理非托管资源。如果析构函数通过某些方法让对 象又变得可访问,那么它就复活了。(译注:析构函数不是用户调用的,也不 由.Net系统调用,而是在由GC产生的额外线程上运行的) 它又活了,但这并不好 。即使是它是从睡眼中唤醒的。这里有一个明显的例子:

public class BadClass
{
// Store a reference to a global object:
private readonly ArrayList _finalizedList;
private string _msg;
public BadClass( ArrayList badList, string msg )
{
// cache the reference:
_finalizedList = badList;
_msg = (string)msg.Clone();
}
~BadClass()
{
// Add this object to the list.
// This object is reachable, no
// longer garbage. It's Back!
_finalizedList.Add( this );

}

当一个BadClass对象的析构函数执行时,它把自己的一 个引用添加到了全局的链表中。这使得它自己又是可达的,它就又活了。前面向 你介绍的这个方法会遇到一些让人畏缩的难题。对象已经被析构了,所以垃圾回 收器从此相信再也不用调用它的析构函数了。如果你实际要析构一个可达对象, 这将不会成功。其次,你的一些资源可能不再有用。GC不再从内存上移除那些只 被析构队列引用的对象,但它们可能已经析构了。如果是这样,它们很可能已经 不能使用了。(译注:也就是说利用上面的那个方法让对象复活后,很有可能对 象是不可用的。)尽管BadClass所拥有的成员还在内存里,它们像是可以被析构 或者处理,但C#语言没有一个方法可以让你控制析构的次序,你不能让这样的结 构可靠的运行。不要尝试。

我还没有看到这样的代码:用这样明显的方 式来复活一个对象,除非是学术上的练习。但我看过这样的代码,析构函数试图 完成一些实质的工作,最后还通过析构函数的调用把引用放到对象中,从而把自 己复活。析构函数里面的代码看上去是精心设计的,另外还有处理函数里的。再 检查一遍,这些代码是做了其它事情,而不是释放资源!这些行为会为你的应用 程序在后期的运行中产生很多BUG。删除这些方法,确保析构函数和Dispose()方 法除了清理资源外,什么也不做。

在托管环境里,你不用为每一个创建 的类写析构函数;只有须要释放一些使用的非托管资源时才添加,或者你的类所 包含的成员有实现了IDisposable接口的时候也要添加。即使如此,你也只用实 现IDisposable接口完成所有的功能就行了,不用析构函数。否则,你会限制你 的派生类实现实现标准的Dispose习惯。 遵守这个我所讲叙的标准的Dispose习 惯。这会让你的程序生活变得轻松,也为你的用户,也为那些从你的类创建派生 类的人。
















本文转自cnn23711151CTO博客,原文链接: http://blog.51cto.com/cnn237111/554662,如需转载请自行联系原作者



相关文章
|
3月前
|
移动开发 前端开发 开发者
Quirks(怪癖)模式是什么?它和 Standards(标准)模式有什么区别?
Quirks(怪癖)模式是什么?它和 Standards(标准)模式有什么区别?
15 0
|
3月前
|
应用服务中间件 Windows
129. SAP ABAP Update Process(更新进程)的概念和设计动机解析
129. SAP ABAP Update Process(更新进程)的概念和设计动机解析
37 1
|
3月前
|
BI
124. SAP ABAP 显式增强技术之 New BAdI 的实战介绍 - 如何创建和激活增强实现
124. SAP ABAP 显式增强技术之 New BAdI 的实战介绍 - 如何创建和激活增强实现
24 0
|
Java
如何实现标准的dispose
如何实现标准的dispose
120 0
|
设计模式 C#
【愚公系列】2021年12月 通用职责分配原则(九)-受保护变量原则(Protected Variations Principle)
【愚公系列】2021年12月 通用职责分配原则(九)-受保护变量原则(Protected Variations Principle)
111 0
【愚公系列】2021年12月 面向对象设计原则(六)-合成复用原则(Composite Reuse Principle or CRP)
【愚公系列】2021年12月 面向对象设计原则(六)-合成复用原则(Composite Reuse Principle or CRP)
|
API 开发者 C#
带你读《More Effective C#:改善C#代码的50个有效方法》之二:API设计
本书围绕一些关于C#和.NET的重要主题,包括C#语言元素、.NET资源管理、使用C#表达设计、创建二进制组件和使用框架等,讲述了最常见的50个问题的解决方案,为程序员提供了改善C#和.NET程序的方法。本书通过将每个条款构建在之前的条款之上,并合理地利用之前的条款,来让读者最大限度地学习书中的内容,为其在不同情况下使用最佳构造提供指导,适合各层次的C#程序员阅读。
一起谈.NET技术,改善代码设计 —— 优化物件之间的特性(Moving Features Between Objects)
  系列博客       1. 改善代码设计 —— 优化函数的构成(Composing Methods)       2. 改善代码设计 —— 优化物件之间的特性(Moving Features Between Objects)       3.
675 0
|
算法 程序员 PHP
程序设计中的计算复用(Computational Reuse)
从斐波那契数列说起 我想几乎每一个程序员对斐波那契(Fibonacci)数列都不会陌生,在很多教科书或文章中涉及到递归或计算复杂性的地方都会将计算斐波那契数列的程序作为经典示例。如果现在让你以最快的速度用C#写出一个计算斐波那契数列第n个数的函数(不考虑参数小于1或结果溢出等异常情况),我不知你的程...
921 0