.NET试图通过免除显式地释放对象所占的内存,来简化对象生命周期管理。但是,与简化管理伴随而来的是潜在的在系统可伸缩性和吞吐量方面的损失。如果对象持有高消耗资源,例如文件,数据库连接等,这些资源只有当Finalize()(或析构器)被调用时才会被释放。资源释放的时机是不确定的,通常是到达某些内存耗尽限定时发生。在理论上,释放对象持有的高消耗资源可能永远不会发生,从而严重阻碍了系统的可伸缩性和吞吐量。
这里有几种解决方法,来处理不确定性终止化带来的问题。这些解决方案称为“deterministic finalization(确定性终止化)”,因为它们发生在一个已知的,确定的时间点上。所以在确定性终止化的技术中,对象都必须显式地通过客户端被告知什么时候它不会再被使用了。
一、Open/Close模式
为了进行确定性终止化,必须首先在对象中实现允许客户端显式命令清理对象持有的高消耗资源的方法。当对象持有的资源可以被释放时,可以使用该模式。在这种情况下,对象应暴露如Open()和Close()这样的方法。一个封装了文件的对象就是个很好的例子。客户端调用在对象上的Close()方法来允许对象释放文件。如果客户端想再一次访问文件,则调用Open()方法,而不需要重新创建对象。实现该模式的典型例子是数据库连接类(如SqlConnection)。
使用Close()带来的主要问题是,使共享对象的客户之间的复杂性比COM的引用计数还要多。客户端不得不协调哪个对象是负责调用的和哪一个是被调用的——也就是说,何时可以安全的地调用Close(),而不影响其它想要使用该对象的客户端。这样就导致了客户端之间的相互耦合。随之而来的一个问题是,例如一些客户端可能只通过对象支持的接口来与对象互动。在这种情况下,应该在哪里实现Open()和Close()?在每个类支持的接口上?还是直接在类上声明公共方法?但无论哪种方法都使客户端与特定的对象终止化机制相耦合。如果机制改变,就会触发在客户端上的级联改变。
二、Dispose()模式
较常见的情况是何时清理对象持有的资源,并且销毁对象并使它不可用。在这种情况下,约定是为对象实现一个Dispose()方法:
void Dispose();
当一个客户端调用Dispose()时,对象应清理所有持有的高消耗资源,并且该客户端不能再次访问对象。实质上,在Dispose()中的代码与Finalize()(或者是析构器)中的代码是一样的,只是使用Finalize()时,清理工作只能在垃圾回收中进行。如果对象的基类也拥有Dispose()方法,那么对象也应调用基类的Dispose()实现来清理基类持有的资源。
三、IDispose模式
有一种更好的方法来决定在什么地方及怎样实现Dispose(),就是把该方法分离到一个单独的接口中去。这个接口是IDisposable,在System命名空间下:
public interface IDisposable
{
void Dispose();
}
public interface IMyInterface
{
void SomeMethod();
}
public class MyClass:IMyInterface,IDisposable
{
public void SomeMethod()
{...}
public void Dispose()
{
//进行对象清理并调用base.Dispose()
}
//More methods and resources
}
在一个分离的接口中定义Dispose()就会允许客户端使用特定域的方法,检测IDispose的存在并调用,使之独立于对象的具体类型和具体的终止化机制:
IMyInterface obj=new MyClass();
obj.SomeMethod();
//客户端想要进行清理工作
IDisposable disposable = obj as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
注意用防御式编程来调用Dispose(),使用as或is。因为客户端不会知道对象是否支持IDisposable接口。使用IDisposable的明显优点就是把客户端与对象终止化机制解耦,并提供一种实现Dispose()的标准途径。但是,它的缺点是在客户端之间共享对象仍旧很麻烦,因为客户端之间不得不协调谁来负责调用IDisposable.Dispose()和何时来调用,因而客户端仍旧相互耦合。总之,在类层次中应以一致的方式来实现IDisposable——就是在每个层次的类中都调用基类的Dispose()。
四、清理和错误处理
不管对象是否实现IDisposable还是仅仅把Dispose()作为一个公共方法,客户端应限定使用对象代码的范围,并在try/finally块中清理对象持有的资源。客户端应把对象的方法调用放在try陈述中,把对Dispose()的调用放在finally陈述中,防止在对象上调用方法时抛出异常。否则,如果发生错误,客户端对Dispose()的调用将永远不会到达:
MyClass obj = new MyClass();
try
{
obj.SomeMethod();
}
finally
{
IDisposable disposable = obj as IDisposable;
if (disposable != null)
{
disposable.Dispose();
}
}
使用这种编程方式的问题是,如果包含多对象调用,那么代码会变得十分散。每个对象都可能抛出异常,都必须在使用完之后清理它们。为了使用恰当的错误处理自动调用Dispose(),C#提供了using机制——可自动生成try/finally模块来使用Dispose()方法,如以下这个类定义:
public class MyClass:IMyInterface,IDisposable
{
public void SomeMethod() {...}
public void Dispose() {...}
//Expensive resources here
}
如果客户端代码是这样:
MyClass obj = new MyClass();
using (obj)
{
obj.SomeMethod();
}
C#编译器会转换代码,语义等价于:
MyClass obj = new MyClass();
try
{
obj.SomeMethod();
}
finally
{
if (obj!=null)
{
IDisposable disposable=obj;
disposable.Dispose();
}
}
也可以累加多个using来处理多个对象:
MyClass obj1 = new MyClass();
MyClass obj2 = new MyClass();
MyClass obj3 = new MyClass();
using (obj1)
using( obj2)
using (obj3)
{
obj1.SomeMethod();
obj2.SomeMethod();
obj3.SomeMethod();
}
using机制与接口
using机制有一个前提:编译生成代码要么可使用类型安全的隐式转换从对象到IDisposable,要么传入的类型提供Dispose()方法。这样是为了在一般情况下防止通过接口来使用using机制,即使该实现类型支持IDisposable:
public interface IMyInterface
{
void SomeMethod();
}
public class MyClass : IMyInterface, IDisposable
{
public void SomeMethod() {...}
public void Dispose() {...}
}
IMyInterface obj = new MyClass();
using (obj) //不会通过编译
{
obj.SomeMethod();
}
三种方式下允许接口与using机制使用。第一种方式,应用程序中的所有接口都继承于IDisosable。缺点是,使接口减少了分离:
public interface IMyInterface:IDisposable
{
void SomeMethod();
}
public class MyClass : IMyInterface
{
public void SomeMethod() {...}
public void Dispose() {...}
}
IMyInterface obj = new MyClass();
using (obj)
{
obj.SomeMethod();
}
第二种方式是强迫类型通过显式转换来使用IDisposable:
public interface IMyInterface
{
void SomeMethod();
}
public class MyClass : IMyInterface, IDisposable
{
public void SomeMethod() {...}
public void Dispose() {...}
}
IMyInterface obj = new MyClass();
using ((IDisposable)obj)
{
obj.SomeMethod();
}
显式转换带来的问题就是类型安全的损失,因为它会导致运行时异常。同时这样也违反了分离接口与实现的原则,以及通过使用支持接口的对象来使客户端与实际终止化机制相解耦的方法。
第三种方式,也是最好的方式,是使用as操作符:
using (obj as IDisposable)
{
obj.SomeMethod();
}
当编译器为using机制生成代码时,会先检查传入的值是否为空,然后通过隐式转换到IDisposable并调用Dispose()。因为,如果传入的类型不支持IDisposable则as操作符会返回null。通过把as操作符引入using机制中,就可以使客户端与具体的终止化机制解耦,并通过防御式的方式来清理对象持有的资源。
using机制与泛型
当提供给using机制的对象是一个泛型类型参数对象是,编译器将没办法知道实际的类型是否支持IDisposable。因此编译器不会允许为using机制指定一个裸露的泛型类型参数:
public class MyClass<T>
{
public void SomeMethod(T t)
{
using (t) //不会通过编译
{...}
}
}
当对象来源于泛型类型参数时,可以约束类型参数支持IDisposable:
public class MyClass<T> where T : IDisposable
{
public void SomeMethod(T t)
{
using (t)
{...}
}
}
约束可以确保客户端只能指定支持IDisposable的类型参数。这样,编译器也会允许在using机制中直接使用类型参数。但是,使用约束带来的问题是不能使用接口作为泛型类型参数了,即使类型支持IDisposable:
public interface IMyInterface { }
public class SomeClass : IMyInterface, IDisposable
{...}
public class MyClass<T> where T : IDisposable
{
public void SomeMethod(T t)
{
using (t)
{...}
}
}
SomeClass someClass = new SomeClass();
MyClass<IMyInterface> obj = new MyClass<IMyInterface>(); //不会通过编译
obj.SomeMethod();
幸运的是,可以同样在泛型类型参数上使用as操作符,并可以在处理接口时使用:
public class MyClass<T> where T : IDisposable
{
public void SomeMethod(T t)
{
using (t as IDisposable)
{...}
}
}
五、Dispose()与Finalize()
Dispose()和Finalize()(或C#析构器)并不矛盾,并且事实上这两个应当同时提供。原因很简单:当有高消耗资源需清理时,即使提供了Dispose()方法也不能保证客户端会调用,以及存在在客户端未处理异常的风险。因此,如果Dispose()不被调用,那么下一步就是使用Finalize()来做资源清理。另一方面,如果Dispose()被调用,在Finalize()之前再就没有对象销毁点了。垃圾收集器从元数据中检测Finalize()的存在。如果检测到,对象就会被添加到一个终结队列等候销毁。作为补偿,如果Dispose()被调用,对象就会通过把自身作为参数调用GC类中的SuppressFinalize()静态方法来禁止终止化操作:
public static void SuppressFinalize(object obj);
还有几件在同时实现Dispose()和Finalize()时需注意的事情。首先,对象应当在同一个辅助方法中引导实现这两个方法,来明确这两个方法都是用来垃圾清理的事实。第二,对象应通过多线程处理多Dispose()调用。对象应在每个方法中检测Dispose()是否准备被调用,如果是则拒绝并抛出异常。最后,对象应正确处理类层次,以及调用基类中的Dispose()和Finalize()。
六、确定性终止化模板
显而易见,在Dispose()和Finalize()的实现中要考虑大量的细节,除非在继承中已包含。下面是作者提供的一个通用模板:
public class BaseClass : IDisposable
{
private bool m_Disposed = false;
protected bool Disposed
{
get
{
lock (this)
{
return m_Disposed;
}
}
}
//不要把Dispose()声明为virtual,防止子类覆写
public void Dispose()
{
lock (this)
{
//检查Dispse()是否准备被调用
if (m_Disposed == false)
{
Cleanup();
m_Disposed = true;
// 把自身从终止化队列中取出,防止第二次执行
GC.SuppressFinalize(this);
}
}
}
protected virtual void Cleanup()
{
/* 在这里进行清理工作 */
}
~BaseClass()
{
Cleanup();
}
public void DoSomething()
{
if (Disposed) //在每个方法中检验
{
throw new ObjectDisposedException("Object is already disposed");
}
}
}
public class SubClass1 : BaseClass
{
protected override void Cleanup()
{
try
{
/* 在这里进行清理工作 */
}
finally
{
//调用基类
base.Cleanup();
}
}
}
public class SubClass2 : BaseClass
{
protected override void Cleanup()
{
try
{
/* 在这里进行清理工作 */
}
finally
{
//调用基类
base.Cleanup();
}
}
}
在类层次中的每一层的Cleanup()都实现自己拥有资源的清理代码。不论调用IDisposable.Dispose()还是Finalize()都被引导到Cleanup()方法中。只有在类层次中的最高层类实现IDisposable。另一方面,最高层基类实现一个非虚拟Dispose()方法,来防止子类覆写。最高层基类的IDisposable.Dispose()实现调用Cleanup()。只能在一个线程的一次执行一个Dispose(),使用了同步锁来防止线程冲突。最高层基类维护m_Disposed属性,标记Dispose()是否准备被调用。第一次Dispose()被调用时设置m_Disposed为true,防止对象自身再一次调用Dispose()。
最高层基类提供一个线程安全,只读属性Disposed。在基类或子类的每一个方法中在执行方法主体之前都要检查该属性,如果Dispose()被调用就抛出一个ObjectDisposedException异常。
注意Cleanup()方法既是虚拟的又是受保护的。虚拟是允许子类重写。受保护是防止客户端使用它。在层次结构中的每个类如果有清理工作要做,都应实现自己版本的Cleanup()。还要注意的是,只在最高层基类中声明析构器,并委托给Cleanup()。如果Dispose()首先被调用,则析构器就不会再被调用,因为Dispose()禁止了终止化。经析构器和经Dispose()调用Cleanup()的唯一不同就是布尔参数m_Disposed,用来让Dispose()知道是否要禁止终止化。
这里是模板中的机制是如何工作的:
1. 客户端创建并使用一个来源于类层次中的对象,并随后通过使用IDisposable或直接调用Dispose()来进行垃圾清理。
2. 无论对象来自类层次中的哪一层,调用的Dispose()方法都是最高层类中的方法,并在其中调用虚拟Cleanup()方法。
3. 调用会传达到可能的最低子类,并调用它的Cleanup()方法。因为在每一次的Cleanup()方法中都调用了基类的Cleanup()方法,每一层都会执行自己的清理工作。
4. 如果客户端从来没有调用Dispose(),则析构器就会调用Cleanup()方法。
注意,该模板可正确处理所有变量类型,实际实例类型的置换和转换:
SubClass
a.Dispose();
SubClass1 b = new SubClass1();
((SubClass2)b).Dispose();
IDisposable c = new SubClass2();
c.Dispose();
SubClass2 d = new SubClass2();
((SubClass1)d).Dispose();
SubClass2 e = new SubClass2();
e.Dispose();
根据原版英文翻译,所以不足和错误之处请大家不吝指正,谢谢:)