4.5. Deterministic Finalization
.NET试图通过减轻程序员显示释放对象已占用内存的工作来使对象生命周期的管理简单化。然而,简化对象生命周期会带来在系统可伸缩性和吞吐量上的潜在惩罚。如果对象持有昂贵的资源,例如文件或数据库连接,这些资源只会在调用Finalize()时被释放(或者C#析构函数)。Finalize()方法(或者C#析构函数)会在一个不确定的时候被调用,通常是在内存消耗达到临界值时。理论上,对象持有的昂贵资源永远不会被释放,因此严重阻碍了系统的可伸缩线和吞吐量。
对于非确定终止化带来的问题,这里有几种解决方案。这些解决方案叫做确定性终止化,因为他们在一个确定的时间点发生。在所有的确定性终止化技术中,当客户端不再需要某个对象时需明确地告诉该对象。本节将描述并对比这些技术。
4.5.1. TheOpen/Close Pattern
为了使确定性终止化工作,首先应该在持有昂贵资源对象中实现一个方法,让客户端明确地执行清理工作。当对象持有的资源可被重新分配时可使用这种模式。在这种情况下,对象应该暴露例如Open()和Close()的方法。
对象封装文件是个不错的例子。当客户端调用Close(),对象将释放文件。如果客户端想再次获取该文件,调用Open()而不用重新创建一个对象。一个实现了这种模式的经典例子是数据库连接类。
使用Close()的主要问题在于在不同客户端共享对象时比COM的引用计数更加复杂。客户端需要协调谁负责调用Close(),什么时候调用Close()不会影响其他还想使用该对象的客户端。结果是,客户端之间产生耦合。此外还存在一些问题。例如,一些客户端仅仅使用对象实现一个接口与对象进行交互。这种情况下,你该在哪里实现Open()与Close()?在该对象支持的每个接口中?或者直接写成类的公有方法?不管你选择哪种方式,都将使客户端与你的特殊对象终止化机制耦合。如果这个机制改变了,所有客户端也需要跟着改变。
4.5.2. TheDispose( ) Pattern
更普遍的例子是处理对象持有的资源时视作销毁对象并使其不可用。这种情况下,我们约定对象必须实现Dispose()方法,定义如下:
void Dispose( );
当客户端调用Dispose()时,对象应该处理它持有的所有资源,并且执行该操作的客户端(其他客户端也一样)不能再试图访问该对象。实质上,Dispose()内的清理工作代码应与Finalize()(或C#析构函数)一样,除了你不用再等待垃圾回收时执行清理工作。
如果对象的基类有Dispose()方法,就该调用基类的Dispose()去处理基类持有的资源。
Dispose()的问题与Close()相似。在客户端之间共享对象会使客户端之间、客户端与对象终止化机制耦合,同样,在哪里实现Dispose()也不清楚。
4.5.3. TheIDisposable Pattern
解决如何实现Dispose()的更好的方式是将这个方法放到一个特殊的接口里。这个特殊的接口(在System命名空间),叫做IDisposable,定义如下:
public interface IDisposable
{
void Dispose( );
}
在对象实现的IDisposable.Dispose()中,对象对持有的所有昂贵资源进行处理:
public interface IMyInterface
{
void SomeMethod( );
}
public class MyClass : IMyInterface,IDisposable
{
public void SomeMethod( )
{...}
public void Dispose( )
{
//Do object cleanup and call base.Dispose( ) if it has one
}
//More methods and resources
}
在一个单独的接口中放置Dispose()方法可以使客户端在用完对象自身的方法后,查询该对象是否支持IDisposable,如果支持则始终调用。这独立于对象的实际类型和实际终止化机制:
IMyInterface obj = new MyClass( );
obj.SomeMethod( );
//Client wants to dispose of whatever needs disposing:
IDisposable disposable = obj as IDisposable;
if(disposable != null)
{
disposable.Dispose( );
}
注意客户端调用Dispose()时使用as操作符的防御性编程方式。客户端并不能确定对象是否支持IDisposable。客户端找到一种安全的方式,因为如果该对象不支持IDisposable,as操作符会返回null。然而,如果该对象支持IDisposable,客户端能迅速处理对象持有的昂贵对象。使用IDisposable的优点在于会降低客户端与对象终止化机制的耦合度,并提供了一种实现Dispose()的标准方式。然而,缺点是在客户端之间共享对象依旧很复杂,因为客户端需要去协调谁负责调用IDisposable.Dispose()以及什么时候调用。因此,客户端之间仍存在耦合。此外,应坚持在类层次中的每层实现IDisposable并调用基层次的Dispose()。
4.5.4.Disposing and Error Handling
不管对象是否提供IDisposable或把Dispose()写成公有方法,客户端应该将使用该对象的代码放在try/finally块中,然后在块中处理该对象持有的资源。客户端应该将方法调用放在try语句中,在finally语句中调用Dispose()。原因在于调用对象的方法可能会发生错误而抛出异常。没有try/finally块,如果错误发生了,客户端永远不会调用处理资源的方法。
MyClass obj = new MyClass( );
try
{
obj.SomeMethod( );
}
finally
{
IDisposable disposable = obj as IDisposable;
if(disposable != null)
{
disposable.Dispose( );
}
}
这种编程模型的问题在于当涉及到多个对象时代码会变得凌乱,因为每一个对象都可能抛出异常,你应该在使用完它们后进行清理工作。为了正确处理错误时自动调用Dispose(),C#提出了using语句,它可以自动生成使用Dispose()方法的try/finally块。例如,一个类的定义如下:
public class MyClass : 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( );
}
4.5.4.1 Theusing statement and interfaces
using语句有一个职责:编译器生成的代码使用了类型安全的隐式转换将对象转换为IDisposable,或者它要求该类型提供了一个Dispose()方法。一般情况下,这阻碍了用接口使用using语句,甚至这个类型支持IDisposable:
public interface IMyInterface
{
void SomeMethod( );
}
public class MyClass: IMyInterface,IDisposable
{
public void SomeMethod( )
{}
publicd Dispose( )
{}
}
IMyInterface obj = new MyClass( ); using(obj)//This line does not compile now
{
obj.SomeMethod( );
}
这里有三种变通方案来结合接口和using语句。第一种是使应用程序中的所有接口从IDisposable继承:
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( );
}
显式转换的问题在于类型安全有损失,因为如果该类型不支持IDisposable,你会在运行时得到一个无效转换异常。只有当你确信该类型支持IDisposable时,你才能将它显示转换为IDisposable。当然,这会使接口的独立性无效,并使客户端与对象使用的终止化机制产生耦合。
第三种变通方法是最好的,它使用了as操作符:
using(obj as IDisposable)
{
obj.SomeMethod( );
}
正如前面所展示的,编译器为using语句生成的代码在将传递的变量隐式转换为IDisposable并调用Dispose()之前将检查这个变量是否为null。因为as操作符将在该类型不支持IDisposable时返回null,将as操作符包含进using语句使客户端与处理的类型及对象实际使用的终止化机制解耦合,并且允许使用防御性编程来处理对象持有的资源。
4.5.4.2 Theusing statements and generics
当你将一个泛型类型参数类型的对象提供给using语句时,编译器无法知道客户端指定的实际类型是否支持IDisposable。因此,编译器不允许你将一个无修饰的泛型类型给using语句:
public class MyClass<T>
{
public void SomeMethod(T t)
{
using(t)//Does not compile
{...}
}
}
当遇到泛型类型参数时,你可以约束该类型参数必须支持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>( ); //Does not compile
obj.SomeMethod(someClass);
幸运的是,你可以在泛型类型参数的using语句中使用as操作符,来实现使用接口的方法:
public class MyClass<T>
{
public void SomeMethod(T t)
{
using(t as IDisposable)
{...}
}
}
4.5.5.Dispose( ) and Finalize( )
Dispose()和Finalize()(或C#析构函数)不是互斥的,事实上,你应该两个都提供。原因很简单:当你有昂贵资源要处理时,即使你提供了Dispose(),依然无法确保客户端调用了它,并且在客户端方面有着未处理异常的风险。因此,如果Dispose()没被调用,备用方案是使用Finalize完成资源清理工作。另一方面,如果Dispose()被调用,延迟对象销毁(释放对象占用的内存)直到Finalize()被调用就没有意义。前面曾提到,垃圾回收器会通过元数据检测到Finalize()的存在。如果检测到Finalize()方法,该对象会加入到终止化队列中并会延迟一段时间才销毁。为了弥补这个问题,如果调用了Dispose(),则该对象应该通过调用GC类的静态方法SuppressFinalize()来忽略终止化,并把自己当做参数传递给该方法:
public static void SuppressFinalize(object obj);
这样就阻止了对象被添加到终止化队列中,就像该对象的定义中不包含Finalize()方法。
当你同时实现Dispose()和Finalize()时还需注意几下几点。第一,对象应该在Dispose()和Finalize()的实现里调用同一个帮助方法,这样无论具体调用哪个方法我们都能做相同的清理工作。第二,要解决可能出现在多个线程上多次调用Dispose()的问题。该对象应该在每个方法中检测是否已经调用Dispose(),如果调用了则拒绝执行方法并抛出一个异常。最后,该对象应该在类层次中适当地调用基类的Dispose()或Finalize()。
4.5.6.Deterministic Finalization Template
毫无疑问,实现Dispose()和Finalize()包含了大量的细节,尤其包含继承时。好消息是,这里提供了一个通用的模板:
public class BaseClass: IDisposable
{
private bool m_Disposed = false;
protected bool Disposed
{
get
{
lock(this)
{
return m_Disposed;
}
}
}
//Do not make Dispose( ) virtual - you should prevent subclasses from overriding
public void Dispose( )
{
lock(this)
{
//Check to see if Dispose( ) has already been called
if(m_Disposed == false)
{
Cleanup( );
m_Disposed = true;
//Take yourself off the finalization queue
//to prevent finalization from executing a second time.
GC.SuppressFinalize(this);
}
}
}
protected virtual void Cleanup( )
{
/*Do cleanup here*/
}
//Destructor will run only if Dispose( ) is not called.
//Do not provide destructors in types derived from this class.
~BaseClass( )
{
Cleanup( );
}
public void DoSomething( )
{
if(Disposed)//verify in every method
{
throw new ObjectDisposedException("Object is already disposed");
}
}
}
public class SubClass1 : BaseClass
{
protected override void Cleanup( )
{
try
{
/*Do cleanup here*/
}
finally
{
//Call base class
base.Cleanup( );
}
}
}
public class SubClass2 : SubClass1
{
protected override void Cleanup( )
{
try
{
/*Do cleanup here*/
}
finally
{
//Call base class
base.Cleanup( );
}
}
}
类层次中每层都在Cleanup()方法中实现自己的资源清理工作代码。调用IDisposable.Dispose()或者析构函数都会去调用Cleanup()方法。只有类层次中最顶端的基类实现了IDisposable,使所有子类对于IDisposable具有多态性。另一方面,最顶端的基类实现的非虚方法Dispose(),这样阻止了子类重写它。最顶端的基类实现IDisposable.Dispose()时调用了Cleanup()。在同一时间只能有一个线程调用Dispose(),因为使用了同步锁。这阻止了两个线程试图同时处理对象而形成的竞争状态。最顶端的基类维持了一个叫做m_Disposed的布尔类型标志,表示Dispose()是否已经被调用。第一次调用Dispose()时,设置m_Disposed为true,这会阻止再次调用Cleanup()。因此,多次调用Dispose()是无害的。
最顶端的基类提供了叫做Disposed的线程安全的只读属性,在基类或子类中的每个方法必须在执行方法体之前检查该属性的值,如果已经调用Dispose()则抛出ObjectDisposedException异常。
注意Cleanup()的修饰符为virtual和protected。标记为虚方法可以让子类去重写它。作为保护成员可以阻止客户端使用它。类层次中的每个类有清理工作时,需要实现自己版本的Cleanup()。同样,需要注意到只有最顶端的基类需要拥有析构函数。且这个析构函数该做的仅仅是调用Cleanup()。在调用Dispose()之后将永远不会调用该析构函数,因为Dispose()中制止了终止化。通过析构函数和Dispose()来调用Cleanup()的不同之处在于布尔类型参数m_Disposed,它会让Dispose()知道是否该制止终止化。
下面是该模板工作时的机制:
1.客户端从类层次中创建并使用一个对象,然后通过IDisposable或直接调用Dispose()。
2.不管该对象来自类层次中的哪层,都由最顶端的基类提供一个调用了虚方法Cleanup()的调用。
3.这个调用传递到正确的子类并调用该类的Cleanup()方法。因为每层的Cleanup()方法都调用了它的基类的Cleanup()方法,所以每层都能执行自己的清理工作。
4.如果客户端从不调用Dispose(),那么析构函数将会调用Cleanup()方法。
注意该模板正确地处理了变量类型,实例化类型以及转换的各种序列:
SubClass1 a = new SubClass2( );
a.Dispose( );
SubClass1 b = new SubClass2( );
((SubClass2)b).Dispose( );
IDisposable c = new SubClass2( );
c.Dispose( );
SubClass2 d = new SubClass2( );
((SubClass1)d).Dispose( );
SubClass2 e = new SubClass2( );
e.Dispose( );