Effective C# Item 15: Utilize using and try/finally for Resource Cleanup
当我们使用非托管资源(unmanaged resources)类型时,应当使用IDisposable接口的Dispose()方法来释放资源。在.Net环境中,对非托管资源的回收不是系统的责任,我们必须自己调用Dispose()方法来释放资源。确保非托管资源会释放的最好方法是使用using或者try/finally。
所有的非托管资源类型都实现了IDisposable接口。另外当我们没有明确的释放资源,比如我们忘记了,C#还会防护性的通过创建终结器(finalizer)来释放资源。如果我们希望应用程序运行的更快时,就应当尽快释放一些不必要的资源。幸运的是在C#中有新的关键字来完成这项任务。我们先考虑下面的代码:
{
SqlConnection myConnection = new SqlConnection(connectString);
SqlCommand myCommand = new SqlCommand(commandString, myConnection);
myConnection.Open();
myCommand.ExecuteNonQuery();
}
有两个对象没有被释放掉:SqlConnection和SqlCommand。它们都会保存在内存中直到终结器被调用为止。
通过下面的修改,我们可以释放它们:
{
SqlConnection myConnection = new SqlConnection(connectString);
SqlCommand myCommand = new SqlCommand(commandString, myConnection);
myConnection.Open();
myCommand.ExecuteNonQuery();
myCommand.Dispose();
myConnection.Dispose();
}
这样做是正确的,但前提是SqlCommand没有抛出异常。一旦出现异常,我们的Dispose()方法就不会运行了。using关键字可以帮助我们确保Dispose()会被运行。当我们使用using的时候,C#的编译器会将它转换成为类似与try/finally的形式:
{
using(SqlConnection myConnection = new SqlConnection(connectString))
{
using(SqlCommand myCommand = new SqlCommand(commandString, myConnection))
{
myConnection.Open();
myCommand.ExecuteNonQuery();
}
}
}
下例中的两段代码会生成非常相似的IL
{
myConnection.Open();
}
try
{
SqlConnection myConnection = new SqlConnection(connectString);
myConnection.Open();
}
finally
{
myConnection.Dispose();
}
当我们使用非托管资源时,使用using是确保资源合理释放的简单途径。如果我们对不支持IDisposable接口的类型使用using关键字,编译器会报错:
using(string msg = "this is a message")
{
Console.WriteLine(msg);
}
另外using只检验编译时类型是否支持IDisposable接口,它不能识别运行时的对象。下例中即便Factory.CreateResource()返回的类型支持IDisposable接口也是不能通过编译的:
using(object obj = Factory.CreateResource)
{
}
对于可能支持可能不支持IDisposable接口的对象,我们可以这样来处理:
using(obj as IDisposable)
{
}
如果对象实现了IDisposable接口,就可以生成释放资源的代码。如果不支持,则生成using(null),虽然不做任何工作,但也是安全的。如果我们拿不准是否应该将对象放在using中,那么比较稳妥的做法是将它放进去。
当我们在程序中使用了非托管资源类型时,我们应当将其放入using的括号中。当有多个需要释放的资源,例如前面的例子中的connection和command,我们应当创建多个using,每一个包含一个对应的对象。这些using会被转化为不同的try/finally块,在效果上看就好像是下面这段代码:
{
SqlConnection myConnection = null;
SqlCommand myCommand = null;
try
{
myConnection = new SqlConnection(connectString);
try
{
myCommand = new SqlCommand(commandString, myConnection);
myConnection.Open();
myCommand.ExecuteNonQuery();
}
finally
{
if(myCommand != null)
{
myCommand.Dispose();
}
}
}
finally
{
if(myConnection != null)
{
myConnection.Dispose();
}
}
}
每个using声明都创建了一个try/finally程序块。我们自己也可以通过这样写来取消多层嵌套:
{
SqlConnection myConnection = null;
SqlCommand myCommand = null;
try
{
myConnection = new SqlConnection(connectString);
myCommand = new SqlCommand(commandString, myConnection);
myConnection.Open();
myCommand.ExecuteNonQuery();
}
finally
{
if(myCommand != null)
{
myCommand.Dispose();
}
if(myConnection != null)
{
myConnection.Dispose();
}
}
}
虽然看起来很简洁,但是我们不要这样使用using声明:
{
SqlConnection myConnection = new SqlConnection(connectString);
SqlCommand myCommand = new SqlCommand(commandString, myConnection);
using(myConnection as IDisposable)
{
using(myCommand as IDisposable)
{
myConnection.Open();
myCommand.ExecuteNonQuery();
}
}
}
这样做是有潜在bug的。一旦SqlCommand的构造函数抛出异常,SqlConnection就无法被释放了。我们必须保证每个非托管资源对象都可以被顺利的释放,否则可能会造成内存资源的浪费。对于单个的非托管资源对象,使用using关键字是最好的方法。对于多个对象,我们可以使用嵌套using或者自己写try/finally的方法来释放资源。
在释放资源上还有一个细节,有些类型不仅有Dispose()方法,还有Close()方法,例如SqlConnection。我们这样可以关闭SqlConnection的连接:
这样做的确能够释放连接,但是并不是和Dispose()方法一样。Dispose()方法除了释放资源之外,还有其他的工作:它会通知垃圾收集器(Garbage Collector)这个对象的资源已经被释放了,而不必在终结器中进行重复的操作。Dispose()调用了GC.SuppressFinalize()方法,这个方法请求系统不要调用指定对象的终结器。而Close()不是这样的。使用Close()释放的对象虽然已经不必调用终结器,但是它还是存在于终结器的释放资源队列当中。Dispose()比Close()的工作做的更加彻底。
Dispose()并没有将对象移出内存。它为对象添加了一个释放资源的钩子(hook)。这就意味着我们可以对正在使用的对象使用Dispose(),我们应当小心这一点。
在C#中大部分的类型都不支持Dispose()。在超过1500种类型中只有100来种实现了IDispose接口。当我们使用实现了这个接口的对象时,我们应当在适当的时候使用using或try/finally块的方法来释放资源。
译自 Effective C#:50 Specific Ways to Improve Your C# Bill Wagner著
回到目录
P.S. 以下为个人想法:
Dispose()和Close()都是可以显示调用来释放资源。而Finalize()是受保护的,析构函数更是不知道什么时候会被执行。被Close()掉的对象是有可能再次复活的,比如SqlConnection,还可以通过Open()继续使用,好像是休眠状态。而被Dispose()掉的对象从理论上来说是不应当再复活了,因为我们在Dispose的同时应当告诉GC这个对象已经over了,以避免重复的对象清除工作。当然我们可以通过释放资源时获得其引用的诡异方法来继续访问对象,但是由于Dispose()调用GC.SuppressFinalize()方法免除终结,因此这些Dispose()掉又复活的对象的资源就再也不会自动被GC释放了。
{
private string insideResources;
public void UseInsideResources()
{
insideResources = "use resouces";
Console.WriteLine(insideResources);
}
public DisposeClass()
{
Console.WriteLine("create object");
}
~DisposeClass()
{
Console.WriteLine("destroy~ object");
}
IDisposable 成员
}
上例中的类在使用Dispose()释放资源后是不会调用析构函数的,没有调用Dispose()时才会在需要终结时调用析构函数。但是如果我们这样使用它:
{
ArrayList myList = new ArrayList();
using(DisposeClass a = new DisposeClass())
{
myList.Add(a);
}
((DisposeClass)(myList[0])).UseInsideResources();
Console.ReadLine();
}
已经被Dispose掉的对象又复活了。而且析构函数不会再被调用。我们同样可以在析构函数中做这种事,只是我们不知道它什么时候会发生而已。
Finalize()是一个神奇的函数,在没有析构函数时它可以像诸如abc()一样做为普通的成员函数,但是一旦析构函数存在,就会报出“已经存在名为Finalize()成员”的错误。更匪夷所思的是当我们将其声明为保护成员函数(开始时我将其声明为public的,不会出现特殊的结果,后来才改成protected)时,它就会被其派生类的析构函数调用。在C#中,派生类的析构函数会自动调用基类的析构函数,因此在这里派生类看来已经把它当作是基类的析构函数了。在编译器中应该将它同析构函数划了等号了吧,可是object.Finalize()明明是不能重写的。对于protected void Finalize()来说,它到底算是个什么东西呢?这应该是在编译阶段就可以发现的错误吧?代码如下:
{
ExtendDisposeClass a = new ExtendDisposeClass();
Console.ReadLine();
}
public class DisposeClass : IDisposable
{
public DisposeClass()
{
Console.WriteLine("create object");
}
// ~DisposeClass()
// {
// Console.WriteLine("destroy~ object");
// }
protected void Finalize()
{
Console.WriteLine("finalize object");
}
IDisposable 成员
}
public class ExtendDisposeClass : DisposeClass
{
~ExtendDisposeClass()
{
Console.WriteLine("destroyExtendDisposeClass object");
}
}
释放个资源可真是不简单啊...