Effective C# 原則15:使用using和try/finally來做資源清理(譯)

來源:互聯網
上載者:User

Effective C# 原則15:使用using和try/finally來做資源清理
Item 15: Utilize using and TRy/finally for Resource Cleanup

使用非託管資源的類型必須實現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的對象,有些時候的釋放是隱式的,當你再釋放一些Null 物件時會出現異常。例如:同一個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.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.