欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C#实现标准的Dispose模式

程序员文章站 2022-06-08 17:44:31
...

上一章说过,对于对象包含非托管资源,要正确的加以清理。对于非托管资源来说,.net framework 会采用一套标准的模式来完成清理工作,因此,如果你编写的类里面用到了非托管资源,那么该类的使用者就会认为这个类同样遵循这套模式。标准的dispose(释放/处置)模式会处理IDisposable接口,又会提供finalizer(终结器/终止化器),以便客户端忘记调用IDisposeable.Dispose()的情况下也可以释放资源。这样做虽然有可能令程序的性能因执行finalizer而下降,但毕竟可以保证垃圾回收器能够把资源回收掉。这是处理非托管资源正确的使用方法,开发者应该透彻地理解该方式。实际上,.net 中的非托管资源还可以通过system.runtime.interop.safehandle 的派生类来访问,哪个类也正确的实现了这套标准的dispose模式。

在类的继承体系中,位于根部的那个基类应该做到以下几点:

		 - 实现IDisposable接口,以便释放资源。
		 - 如果代码本身含有非托管资源,那就添加finalizer,以防客户端忘记调用Dispose()方法。若是没有非托管资源,则不用添加finalizer。
		 - Dispose方法与finalizer(如果有的话)都把释放资源的工作委托给虚方法,使得子类能够重写该方法,以释放他们自己得资源。

继承体系中得子类应该做到以下几点:

		 - 如果子类有自己得资源需要释放,那就重写由基类所定义得那个虚方法,若没有,则不用重写该方法。
		 - 如果子类自身得某个成员字段表示的是非托管资源,那就实现finalizer,若没有这样的字段,则不用实现finalizer。
		 - 记得调用基类的同名函数。

首先要注意,如果你的类本身不包含非托管资源,那就不用编写finalizer,但若是包含这种资源的话,就必须提供finalizer,因为你不能保证该类的使用者总是会调用Dispose()方法。如果他们忘了,就会造成资源泄漏,尽管这是他们的错,但受责备的确实你(因为你没有提前防范这种情况)。只有提供了finalizer,才能够确保非托管资源总能够得以释放。因此,只要你的类含有非托管资源,就一定要提供finalizer。 垃圾回收器每次运行的时候,都会把不带finalizer的垃圾对象立刻从内存中移走,而带有finalizer的对象则会继续留在内存里,而且会添加至队列中。GC会安排线程在这些对象上面运行 finalizer,运行完之后,通常就可以像哪些不带finalizer的垃圾对象一样从内存中移走。然而与那些对象相比,它们属于老一代的对象,只有当其finalizer执行过一次后,GC才将其视为可以直接释放的对象,这意味着它们需要在内存中停留更长的时间。这也是没有办法的事情,因为你必须通过finalizer这 种防范机制来确保非托管资源得以释放。尽管程序性能或许会有所下降,但这并不值得过分担忧,只要客户代码能像平常那样记得调用Dispose()方法,就不会有这个问题。如果你所编写的类使用了某些必须要即时释放的资源,那就应该按照惯例实现IDisposable接口,以提醒本类的使用者与运行期系统注意。该接口只包含一个方法:

public interface IDisposabel
{
	void Dispose();
}

实现IDisposable.Dispose()方法时,要注意以下四点:

1. 把非托管资源全部释放掉。
2. 把托管资源全部释放掉。
3. 设定相关的状态标志,用以表示该对象已经清理过了。如果对象已经清理过了之后还有人要访问其中的公共成员,那么你就要通过此标志得知这一状况,从而令这些操作抛出objectDisposabledException。
4. 阻止垃圾回收器重复清理该对象。可以通过GC.SupperssFinalize(this)来完成。

正确实现IDisposable接口是一举两得的事情,因为它既要提供适当的机制使得托管资源能够即时释放,又令客户端可以通过标准的Disposse()方法来释放非托管型资源。这是相当好的效果。如果你所编写的类实现了IDisposable接口,并且客户端又能够记得调用其Dispose()方法,那么程序就不必执行finalizer了,其性能也不会因此而下降,这使得该类能够顺利融入 .Net环境中。
但是这种机制依然有漏洞,因为子类在清理自身的资源时还必须保证基类的资源也得到清理。如果子类要重写finalizer或者根据自己的需要给IDisposable.Disppose()添加新的逻辑,那就必须调用基类的版本,否则,基类的资源就无法正确释放。此外,由于finalizer与Dispose()都有一些类似的任务需要完成,因此,这两方法几乎总是包含重复的代码。直接重写接口中的函数可能无法达成你想要的效果,因为这些函数在默认情况下是非虚的。为此,还需要再做一点工作来解决这些问题:也就是要把finalizer与Dispose()中重复的代码提取到protected级别的虚函数里面,使得子类能够重写该函数,以释放他们自己所分配的那些资源,而基类则应该在接口方法里面把核心逻辑实现好。这个辅助的虚函数可以申明成下面的样子供子类去重写,使他们能够在Dispose()方法或finalizer得以执行时把相关的资源清理干净:

protected virtual void Dispose(bool isDispposing)

IDisposable.Dispose()与finalizer都可以调用该方法来清理相关的资源。这个方法与IDisposable.Dispose()相互重载,然而由于它是一个虚方法,因此,子类可以重写该方法,以便用适当的代码来清理自身的资源并调用基类的版本。如果isDisposing参数是true,那么应该同时清理托管资源和非托管资源。反之,若为false,则只应清理非托管资源(因为这表明该方法是在finalizer中调用的)。无论是哪一种情况,都要调用基类的Dispose(bool) 方法,使得基类有机会清理其资源。
下面这个简单的示范演示实现该模式所用的代码框架,其中MyResourceHog类实现了IDisposable接口,并创建了DIspose(bool)这个虚方法:

public class MyResourceHog:IDisposable
{
	//Flag for already disposed
	private bool alreadyDisposed=false;
	
	//Implementation of IDisposable
	//Call the virtual Dispose method
	//Suppress FinaLization
	public void Dispose()
	{
		Dispose(true);
		GC.SupperssFinalize(this);
	}
	
	//Virtual Dispose method
	proected virtual void Dispose(bool isDisposing)
	{
		//Don't dispose more than once
		if(alreadyDisposed)
			return;
		if(isDisposing)
		{
			//alied:free managed resources here.
		}
		//elided:free unmanaged resources here.
		//set disposed flag.
		alreadyDisposed=true;
	}
	
	public void ExampleMethod()
	{
		if(alreadyDisposed)
		{
			throw new ObjectDisposedException("MyResourceHog","Called Example Method on Disposed object");
			//remainder elided
		}
	}
}

DerivedResourceHog类继承了MyResourceHog,并重写了基类中的protected Dispose(bool)方法:

public class DerivedResourceHog:MyResourceHog
{
	//have its own disposed flag.
	private bool disposed=false;

	protected override void Dispose(bool isDisposing)
	{
		//don't disppose more than once.
		if(disposed)
		{
			return;
		}
		if(isDisposing)
		{
			//TOPO:free manage resources here .
		}
		//TODO:free unmanaged resources here.

		//let the base class free its resources.
		//base class is responsible for calling
		//GC.SupperssFinalize()
		base.Dispose(isDisposing);
	
		//set dervied class disposed flag:
		disposed=true;
	}
}

请注意,基类与子类对象采用各自的disposed标志来表示其资源是否得到释放,这么写是为了防止出错。假如共用同一个标志,那么子类就有可能在释放自己的资源时率先把标志设置成true,而等到基类运行Dispose(bool)方法时,则会误以为其资源已经释放过了。
Dispose(bool) 与finalizer 都必须编写得很可靠,也就是要具被幂等(idempotent)的性质,这意味着多次调用Dispose(bool)的效果与只调用一次的效果应该是完全相同的。由于各对象的dispose操作之间可能没有明确的顺序,因此在执行自身的Dispose(bool) 时,或许 会发现其中某个成员对象已经dispose(释放)过了。这并不表示程序出现了问题,因为Dispose()方法本来就有可能多次得到调用。对于该方法以外的其它pubic 方法来说,如果在对象已经遭到释放之后还有人要调用该对象,那就应该抛出ObjectDisposedException,然而Disppose()是个例外。在对象遭到释放之后调用该方法不应该有任何效果。当系统执行某个对象的finalizer时,该对象所引用的某些资源可能已经释放过了,或是从未得到初始化。对于前者来说是不用检查其是否为null的,因为它所引用的那个资源肯定还可以继续引用,只不过有可能已经释放掉了,甚至其finalizer都有可能已经执行过了。
MyResourceHog与DerivedResourceHog这两个类都没有编写finalizer,由于笔者所举的这段范例代码并未直接包含非托管资源,因此用不着编写finalizer,这就是说,范例代码根本不会以false为参数来调用Dispose(bool)。这是正确的,因为只有当该类型直接含有非托管资源时,才应该实现finalizer。否则的话,即便不调用,也会给这个累心再增加负担,因为它毕竟是有较大开销的。如果类里面确实有非托管资源,那就必须添加finalizer才能够正确的实现dispose模式,此时的finalizer应该与Dispose(bool)一样,都可以适当的将非托管资源释放掉。
在编写Dispose或finalizer等资源清理方法时,最重要的一点是:只应该释放资源,而不是应该做其它的处理,否则,就会产生一些涉及对象生存期的严重问题。按道理来说,对象应该在构造时诞生,并在变成垃圾且遭到回收时消亡。如果程序不再访问某个对象,那么可以认为该对象已经昏迷,对象中的方法也不会得到调用,这实际上就等于已经消亡了。然而如果它包含finalizer,那么系统在正式宣告其消亡之前,会给它留一个机会,使之能够将非托管资源清理掉。此时,如果finalizer令该对象可以重视为程序所引用,那么它就复活了,可是这种从昏迷中醒过来的对象活得并不好,下面举一个例子:

public class BadClass
{
	//store a reference to a glabal object
	private static readonly List<BadClass> finalizedList=new List<BadClass>();
	private string msg;

	public BadClass(string msg)
	{
		//cache the reference:
		msg=(string)msg.Clone();
	}

	~BadClass()
	{
		//add this object to the list.
		//This object is reachable,no
		//longer garbage .It's Back!
		finalizerList.Add(this);
	}
}

BadClass对象执行其finalizer时,会把指向自身的引用添加到全局列表中,使得程序能够再度访问该对象,从而令这个对象得以复活。这会造成很大的问题。首先,由于对象的finalizer已经执行过了,因此垃圾回收器不会再执行其finalizer,于是,这个复活的对象就不会为系统所终结。其次,该对象所引用的资源可能已经无效了。对于那些只能够由finalizer队列中的对象所访问的资源来说,GC并不会将其从内存中移除,然而这些资源的finalizer或许已经执行过了,如果是这样,那么这些资源基本上就不能再使用了。由此可见,尽管BadClass对象的某些成员依然位于内存中,但或许已经为系统所释放或终结了,而其终结的顺序是开发者所无法控制的。由于C#语言并没有提供相应的控制机制,因此,这样写出来的程序是不可靠的。请大家不要用这种写法。
对于以运行在托管环境中的程序来说,开发者并不需要给自己所创建的每一个类型都编写finalizer。只有当其中包含有非托管资源或是带有实现了IDisposable接口的成员时,才需要添加finalizer。然而要注意:在只实现IDisposable接口但并不需要finalizer的场合下,还是应该把整套模式实现出来,否则,子类就无法轻松实现标准的dispose方案。因此,你应该像笔者在本条中所说的这样,把标准的dispose框架写好,这不仅会使你的工作更加顺利,而且能令该类的用户以及从中派生子类的开发者更为方便地利用它。

.