Effective C# 原则15:使用using和try/finally来做资源清理

使用非托管资源的类型必须实现IDisposable接口的Dispose()方法来精确的释放系统资源。.Net环境的这一规则使得释放资源代码的职责是类型的使用者,而不是类型或系统。因此,任何时候你在使用一个有Dispose()方法的类型时,你就有责任来调用Dispose()方法来释放资源。最好的方法来保证Dispose()被调用的结构是使用using语句或者try/finally块。

所有包含非托管资源的类型应该实现IDisposable接口,另外,当你忘记恰当的处理这些类型时,它们会被动的创建析构函数。如果你忘记处理这些对象,那些非内存资源会在晚些时候,析构函数被确切调用时得到释放。这就使得这些对象在内存时待的时间更长,从而会使你的应用程序会因系统资源占用太多而速度下降。

幸运的是,C#语言的设计者精确的释放资源是一个常见的任务。他们添加了一个关键字来使这变得简单了。
假设你写了下面的代码:

public void ExecuteCommand( string connString,
  string commandString )
{
  SqlConnection myConnection = new SqlConnection( connString );
  SqlCommand mySqlCommand = new SqlCommand( commandString,
    myConnection );

  myConnection.Open();
  mySqlCommand.ExecuteNonQuery();
}

这个例子中的两个可处理对象没有被恰当的释放:SqlConnection和SqlCommand。两个对象同时保存在内存里直到析构函数被调用。(这两个类都是从System.ComponentModel.Component继承来的。)

解决这个问题的方法就是在使用完命令和链接后就调用它们的Dispose:

public void ExecuteCommand( string connString,
  string commandString )
{
  SqlConnection myConnection = new SqlConnection( connString );
  SqlCommand mySqlCommand = new SqlCommand( commandString,
    myConnection );

  myConnection.Open();
  mySqlCommand.ExecuteNonQuery();

  mySqlCommand.Dispose( );
  myConnection.Dispose( );
}

这很好,除非SQL命令在执行时抛出异常,这时你的Dispose()调用就永远不会成功。using语句可以确保Dispose()方法被调用。当你把对象分配到using语句内时,C#的编译器就把这些对象放到一个try/finally块内:

public void ExecuteCommand( string connString,
  string commandString )
{
  using ( SqlConnection myConnection = new
    SqlConnection( connString ))
  {
    using ( SqlCommand mySqlCommand = new
      SqlCommand( commandString,
      myConnection ))
    {
      myConnection.Open();
      mySqlCommand.ExecuteNonQuery();
    }
  }
}

当你在一个函数内使用一个可处理对象时,using语句是最简单的方法来保证这个对象被恰当的处理掉。当这些对象被分配时,会被编译器放到一个try/finally块中。下面的两段代码编译成的IL是一样的:

SqlConnection myConnection = null;

// Example Using clause:
using ( myConnection = new SqlConnection( connString ))
{
  myConnection.Open();
}


// example Try / Catch block:
try {
  myConnection = new SqlConnection( connString );
  myConnection.Open();
}
finally {
  myConnection.Dispose( );
}

(译注:就我个人对try/catch/finally块的使用经验而言,我觉得上面这样的做法非常不方便。可以保证资源得到释放,却无法发现错误。关于如何同时抛出异常又释放资源的方法可以参考一下其它相关资源,如Jeffrey的.Net框架程序设计,修订版)

如果你把一个不能处理类型的变量放置在using语句内,C#编译器给出一个错误,例如:

// Does not compile:
// String is sealed, and does not support IDisposable.
using( string msg = "This is a message" )
  Console.WriteLine( msg );

using只能在编译时,那些支持IDispose接口的类型可以使用,并不是任意的对象:

// Does not compile.
// Object does not support IDisposable.
using ( object obj = Factory.CreateResource( ))
  Console.WriteLine( obj.ToString( ));

如果obj实现了IDispose接口,那么using语句就会生成资源清理代码,如果不是,using就退化成使用using(null),这是安全的,但没有任何作用。如果你对一个对象是否应该放在using语句中不是很确定,宁可为了更安全:假设要这样做,而且按前面的方法把它放到using语句中。

这里讲了一个简单的情况:无论何时,当你在某个方法内使用一个可处理对象时,把这个对象放在using语句内。现在你学习一些更复杂的应用。还是前面那个例子里须要释放的两个对象:链接和命令。前面的例子告诉你创建了两个不同的using语句,一个包含一个可处理对象。每个using语句就生成了一个不同的try/finally块。等效的你写了这样的代码:

public void ExecuteCommand( string connString,
  string commandString )
{
  SqlConnection myConnection = null;
  SqlCommand mySqlCommand = null;
  try
  {
    myConnection = new SqlConnection( connString );
    try
    {
      mySqlCommand = new SqlCommand( commandString,
      myConnection );

      myConnection.Open();
      mySqlCommand.ExecuteNonQuery();
    }
    finally
    {
      if ( mySqlCommand != null )
        mySqlCommand.Dispose( );
    }
  }
  finally
  {
    if ( myConnection != null )
      myConnection.Dispose( );
  }
}

每一个using语句生成了一个新的嵌套的try/finally块。我发现这是很糟糕的结构,所以,如果是遇到多个实现了IDisposable接口的对象时,我更愿意写自己的try/finally块:

public void ExecuteCommand( string connString,
  string commandString )
{
  SqlConnection myConnection = null;
  SqlCommand mySqlCommand = null;
  try {
    myConnection = new SqlConnection( connString );
    mySqlCommand = new SqlCommand( commandString,
      myConnection );

    myConnection.Open();
    mySqlCommand.ExecuteNonQuery();
  }
  finally
  {
    if ( mySqlCommand != null )
      mySqlCommand.Dispose();
    if ( myConnection != null )
      myConnection.Dispose();
  }
}

(译注:作者里的判断对象是否为null是很重要的,特别是一些封装了COM的对象,有些时候的释放是隐式的,当你再释放一些空对象时会出现异常。例如:同一个COM被两个不同接口的变量引用时,在其中一个上调用了Dispose后,另一个的调用就会失败。在.Net里也要注意这样的问题,所以要判断对象是否为null)

然而,请不要自作聪明试图用as来写这样的using语句:

public void ExecuteCommand( string connString,
  string commandString )
{
  // Bad idea. Potential resource leak lurks!
  SqlConnection myConnection =
    new SqlConnection( connString );
  SqlCommand mySqlCommand = new SqlCommand( commandString,
      myConnection );
      using ( myConnection as IDisposable )
      using (mySqlCommand as IDisposable )
      {
        myConnection.Open();
        mySqlCommand.ExecuteNonQuery();
      }

}

这看上去很清爽,但有一个狡猾的(subtle )的bug。 如果SqlCommand()的构造函数抛出异常,那么SqlConnection对象就不可能被处理了。你必须确保每一个实现了IDispose接口的对象分配在在using范围内,或者在try/finally块内。否则会出现资源泄漏。

目前为止,你已经学会了两种最常见的情况。无论何时在一个方法内处理一个对象时,使用using语句是最好的方法来确保申请的资源在各种情况下都得到释放。当你在一个方法里分配了多个(实现了IDisposable接口的)对象时,创建多个using块或者使用你自己的try/finally块。

对可处理对象的理解有一点点细微的区别。有一些对象同时支持Disponse和Close两个方法来释放资源。SqlConnection就是其中之一,你可以像这样关闭SqlConnection:

public void ExecuteCommand( string connString,
  string commandString )
{
  SqlConnection myConnection = null;
  try {
    myConnection = new SqlConnection( connString );
    SqlCommand mySqlCommand = new SqlCommand( commandString,
      myConnection );

    myConnection.Open();
    mySqlCommand.ExecuteNonQuery();
  }
  finally
  {
    if ( myConnection != null )
      myConnection.Close();
  }
}

这个版本关闭了链接,但它确实与处理对象是不一样的。Dispose方法会释放更多的资源,它还会告诉GC,这个对象已经不再须要析构了(译注:关于C#里的析构,可以参考其它方面的书籍)。Dispose会调用GC.SuppressFinalize(),但Close()一般不会。结果就是,对象会到析构队列中排队,即使析构并不是须要的。当你有选择时,Dispose()比Colse()要好。你会在原则18里学习更更精彩的内容。

Dispose()并不会从内存里把对象移走,对于让对象释放非托管资源来说是一个hook。这就是说你可能遇到这样的难题,就是释放一个还在使用的对象。不要释放一个在程序其它地方还在引用的对象。

在某些情况下,C#里的资源管理比C++还要困难。你不能指望确定的析构函数来清理你所使用的所有资源。但垃圾回收器却让你更轻松,你的大从数类型不必实现IDisposable接口。在.Net框架里的1500多个类中,只有不到100个类实现了IDisposable接口。当你使用一个实现了IDisposeable接口的对象时,记得在所有的类里都要处理它们。你应该把它们包含在using语句中,或者try/finally块中。不管用哪一种,请确保每时每刻对象都得到了正确的释放。

========================================================

Item 15: Utilize using and TRy/finally for Resource Cleanup
Types that use unmanaged system resources should be explicitly released using the Dispose() method of the IDisposable interface. The rules of the .NET environment make that the responsibility of the code that uses the type, not the responsibility of the type or the system. Therefore, anytime you use types that have a Dispose() method, it's your responsibility to release those resources by calling Dispose(). The best way to ensure that Dispose() always gets called is to utilize the using statement or a try/finally block.

All types that own unmanaged resources implement the IDisposable interface. In addition, they defensively create a finalizer for those times when you forget to dispose properly. If you forget to dispose of those items, those nonmemory resources are freed later, when finalizers get their chance to execute. All those objects then stay in memory that much longer, and your application becomes a slowly executing resource hog.

Luckily for you, the C# language designers knew that explicitly releasing resources would be a common task. They added keywords to the language that make it easy.

Suppose you wrote this code:

public void ExecuteCommand( string connString,
  string commandString )
{
  SqlConnection myConnection = new SqlConnection( connString );
  SqlCommand mySqlCommand = new SqlCommand( commandString,
    myConnection );

  myConnection.Open();
  mySqlCommand.ExecuteNonQuery();
}

Two disposable objects are not properly cleaned up in this example: SqlConnection and SqlCommand. Both of these objects remain in memory until their finalizers are called. (Both of these classes inherit their finalizer from System.ComponentModel.Component.)

You fix this problem by calling Dispose when you are finished with the command and the connection:

public void ExecuteCommand( string connString,
  string commandString )
{
  SqlConnection myConnection = new SqlConnection( connString );
  SqlCommand mySqlCommand = new SqlCommand( commandString,
    myConnection );

  myConnection.Open();
  mySqlCommand.ExecuteNonQuery();

  mySqlCommand.Dispose( );
  myConnection.Dispose( );
}

That's fine, unless any exceptions get thrown while the SQL command executes. In that case, your calls to Dispose() never happen. The using statement ensures that Dispose() is called. You allocate an object inside a using statement, and the C# compiler generates a try/finally block around each object:

public void ExecuteCommand( string connString,
  string commandString )
{
  using ( SqlConnection myConnection = new
    SqlConnection( connString ))
  {
    using ( SqlCommand mySqlCommand = new
      SqlCommand( commandString,
      myConnection ))
    {
      myConnection.Open();
      mySqlCommand.ExecuteNonQuery();
    }
  }
}

Whenever you use one Disposable object in a function, the using clause is the simplest method to use to ensure that objects get disposed of properly. The using statement generates a TRy/finally block around the object being allocated. These two blocks generate exactly the same IL:

SqlConnection myConnection = null;

// Example Using clause:
using ( myConnection = new SqlConnection( connString ))
{
  myConnection.Open();
}

// example Try / Catch block:
try {
  myConnection = new SqlConnection( connString );
  myConnection.Open();
}
finally {
  myConnection.Dispose( );
}

If you use the using statement with a variable of a type that does not support the IDisposable interface, the C# compiler generates an error. For example:

// Does not compile:
// String is sealed, and does not support IDisposable.
using( string msg = "This is a message" )
  Console.WriteLine( msg );

The using statement works only if the compile-time type supports the IDisposable interface. You cannot use it with arbitrary objects:

// Does not compile.
// Object does not support IDisposable.
using ( object obj = Factory.CreateResource( ))
  Console.WriteLine( obj.ToString( ));

A quick defensive as clause is all you need to safely dispose of objects that might or might not implement IDisposable:

// The correct fix.
// Object may or may not support IDisposable.
object obj = Factory.CreateResource( );
using ( obj as IDisposable )
  Console.WriteLine( obj.ToString( ));

If obj implements IDisposable, the using statement generates the cleanup code. If not, the using statement degenerates to using(null), which is safe but doesn't do anything. If you're not sure whether you should wrap an object in a using block, err on the side of safety: Assume that it does and wrap it in the using clause shown earlier.

That covers the simple case: Whenever you use one disposable object that is local to a method, wrap that one object in a using statement. Now you can look at a few more complicated usages. Two different objects need to be disposed in that first example: the connection and the command. The example I showed you creates two different using statements, one wrapping each of the two objects that need to be disposed. Each using statement generates a different try/finally block. In effect, you have written this construct:

public void ExecuteCommand( string connString,
  string commandString )
{
  SqlConnection myConnection = null;
  SqlCommand mySqlCommand = null;
  try
  {
    myConnection = new SqlConnection( connString );
    try
    {
      mySqlCommand = new SqlCommand( commandString,
      myConnection );

      myConnection.Open();
      mySqlCommand.ExecuteNonQuery();
    }
    finally
    {
      if ( mySqlCommand != null )
        mySqlCommand.Dispose( );
    }
  }
  finally
  {
    if ( myConnection != null )
      myConnection.Dispose( );
  }
}

Every using statement creates a new nested TRy/finally block. I find that an ugly construct, so when I allocate multiple objects that implement IDisposable, I prefer to write my own try/finally blocks:

public void ExecuteCommand( string connString,
  string commandString )
{
  SqlConnection myConnection = null;
  SqlCommand mySqlCommand = null;
  try {
    myConnection = new SqlConnection( connString );
    mySqlCommand = new SqlCommand( commandString,
      myConnection );

    myConnection.Open();
    mySqlCommand.ExecuteNonQuery();
  }
  finally
  {
    if ( mySqlCommand != null )
      mySqlCommand.Dispose();
    if ( myConnection != null )
      myConnection.Dispose();
  }
}

However, don't get too cute and try to build one using clause with as statements:

public void ExecuteCommand( string connString,
  string commandString )
{
  // Bad idea. Potential resource leak lurks!
  SqlConnection myConnection =
    new SqlConnection( connString );
  SqlCommand mySqlCommand = new SqlCommand( commandString,
      myConnection );
      using ( myConnection as IDisposable )
      using (mySqlCommand as IDisposable )
      {
        myConnection.Open();
        mySqlCommand.ExecuteNonQuery();
      }

}

It looks cleaner, but it has a subtle bug. The SqlConnection object never gets disposed if the SqlCommand() constructor throws an exception. You must make sure that any objects that implement IDisposable are allocated inside the scope of a using block or a try block. Otherwise, resource leaks can occur.

So far, you've handled the two most obvious cases. Whenever you allocate one disposable object in a method, the using statement is the best way to ensure that the resources you've allocated are freed in all cases. When you allocate multiple objects in the same method, create multiple using blocks or write your own single try/finally block.

There is one more nuance to freeing disposable objects. Some types support both a Dispose method and a Close method to free resources. SqlConnection is one of those classes. You could close SqlConnection like this:

public void ExecuteCommand( string connString,
  string commandString )
{
  SqlConnection myConnection = null;
  try {
    myConnection = new SqlConnection( connString );
    SqlCommand mySqlCommand = new SqlCommand( commandString,
      myConnection );

    myConnection.Open();
    mySqlCommand.ExecuteNonQuery();
  }
  finally
  {
    if ( myConnection != null )
      myConnection.Close();
  }
}

This version does close the connection, but that's not exactly the same as disposing of it. The Dispose method does more than free resources: It also notifies the Garbage Collector that the object no longer needs to be finalized. Dispose calls GC.SuppressFinalize(). Close typically does not. As a result, the object remains in the finalization queue, even though finalization is not needed. When you have the choice, Dispose() is better than Close(). You'll learn all the gory details in Item 18.

Dispose() does not remove objects from memory. It is a hook to let objects release unmanaged resources. That means you can get into trouble by disposing of objects that are still in use. Do not dispose of objects that are still being referenced elsewhere in your program.

In some ways, resource management can be more difficult in C# than it was in C++. You can't rely on deterministic finalization to clean up every resource you use. But a garbage-collected environment really is much simpler for you. The vast majority of the types you make use of do not implement IDisposable. Less than 100 classes in the .NET Framework implement IDisposablethat's out of more than 1,500 types. When you use the ones that do implement IDisposable, remember to dispose of them in all cases. You should wrap those objects in using clauses or TRy/finally blocks. Whichever you use, make sure that objects get disposed properly all the time, every time.

评论: 0 | 引用: 0 | 查看次数: 4040
发表评论
登录后再发表评论!