using語句之-釋放資源和異常處理

using語句之-釋放資源和異常處理

2009-05-23 14:04

       有時,代碼要求非託管資源,如文件句柄、COM 包裝或 SQL 連接。在使用一個或多個此類資源完成了代碼後,Using 塊確保這些資源的釋放。這樣,其他代碼就可以使用它們。
託管資源由 .NET Framework 垃圾回收器 (GC)釋放,您不需要進行任何額外的編碼。您也不需要用於託管資源的Using 塊。
Using 塊有三個部分:獲取、使用和釋放。

  • 獲取表示創建變量並將其初始化,以便引用系統資源。Using 語句可獲取一個或多個資源,您可以在進入塊之前恰好獲取一個資源,並將其提供給 Using 語句。如果提供 resourceexpression,在將控制權傳遞給Using 語句之前,您必須獲取資源。
  • 使用表示訪問資源並使用資源執行操作。Using 和 End Using 之間的語句代表資源的使用過程。
  • 釋放表示針對 resourcename 中的對象調用 Dispose 方法。這使該對象可以完全終止其非託管資源。End Using 語句釋放 Using 塊控制的資源。

行爲Using 塊的工作方式類似於 Try...Finally 構造,在該構造中,Try 塊使用資源,而 Finally 塊釋放資源。因此,不管您如何退出塊,Using 塊都可確保資源的釋放。即使發生未處理的異常(除 StackOverflowException 外),也是如此。
Using 語句獲取的每個資源變量的範圍僅限於 Using 塊。
如果在 Using 語句中指定多個系統資源,效果就好像您將多個Using 塊相互嵌套一樣。

Using 塊中的結構化異常處理如果需要處理可能發生在 Using 塊中的異常,可以向該塊中添加一個完整的Try...Finally 構造。如果需要處理 Using 語句未成功獲取資源的情況,可以進行測試,以確定 resourcename 是否爲Nothing

進行結構化異常處理而不使用 Using 塊如果需要對資源的獲取進行更細緻的控制,或者需要 Finally 塊中的附加代碼,可以將 Using 塊重寫爲 Try...Finally 構造。下面的示例顯示主幹 Try 和 Using 構造,這兩個構造在獲取和釋放 resource 過程中是等效的。

   在using語句內的資源必須重載IDispose這個接口,上面說的可以看出,using只負責執行所包含資源的一些操作,然後釋放掉,並不是能夠處理(using(){。。。。})大括號裏的異常,如果擔心內部發生異常,是要自己寫try和catch的。。。using System;

class C :IDisposable
{
public voidUseLimitedResource()
{
Console.WriteLine("Usinglimited resource...");
}

voidIDisposable.Dispose()
{
Console.WriteLine("Disposinglimited resource.");
}
}

class Program
{
static voidMain()
{
using (C c =new C())
{
c.UseLimitedResource();
}
Console.WriteLine("Nowoutside using statement.");
Console.ReadLine();
}
}


系列二:資源管理(利用using,try/finally,減少內存垃圾)

1.我們常使用IDisposable接口的Dispose()方法來精確的釋放非託管系統資源。在日常代碼中我們確保調用Dispose()的方法是用using和try/catch/finally語句。

 

2.所有包含非託管資源的類型都會實現IDisposable接口,他們還會創建終結器,以防止我們忘記調用Dispose()。如果你忘記調用Dispose(),那些非內存資源會在晚些時候,終結器會調用時會釋放這些資源。這就使得這些對象在內存時待的時間更長,從而會使你的應用程序會因系統資源佔用太多而速度下降。

 

3.代碼展示:

public void ExecuteCommand( string connString,stringcommandString )
{
    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,stringcommandString )
{
     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,stringcommandString )
{
    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( );
}

 

4.我們不能對一個沒有實現IDispose接口的對象使用using方式,編譯器會報錯。

 

5.每一個using語句生成了一個新的嵌套的try/finally塊。我發現這是很糟糕的結構,所以,如果是遇到多個實現了IDisposable接口的對象時,我更願意寫自己的try/finally塊:

public void ExecuteCommand( string connString,stringcommandString )
{
     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();
        }
}

 

6.有一些對象同時支持Disponse和Close兩個方法來釋放資源。SqlConnection就是其中之一,你可以像這樣關閉

public void ExecuteCommand( string connString,stringcommandString )
{
      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,這個對象已經不再須要析構了(注:這個對象本身並沒有刪除出內存)。它是通過調用GC.SuppressFinalize()來實現這一點的。但Close()一般不會,因此調用了close()的對象仍然留在終結列表中,雖然這個時候不需要終結器的操作了。當你有選擇時,Dispose()比Colse()要好。

 

 7.我們應該對經常出現的局部變量提升爲類的成員變量(當在一個方法中經常調用一個變量時候,方法調用結束後,垃圾收集器就會去處理這個變量,每調用一次垃圾收集器就要增加額外的開銷),對那些是非託管資源的變量升級爲成員變量我們還應該把類設計成繼承IDisposable接口,在接口方法中釋放非託管資源。下面舉例說明:

 

protected override void OnPaint( PaintEventArgs e )
{
using ( Font MyFont = new Font( "Arial", 10.0f ))
{
    e.Graphics.DrawString( DateTime.Now.ToString(),
      MyFont, Brushes.Black, new PointF( 0,0 ));
}
base.OnPaint( e );
}

OnPaint()函數的調用很頻繁的,每次調用它的時候,都會生成另一個Font對象,而實際上它是完全一樣的內容。垃圾回收器每次都須要清理這些對象。這將是難以置信的低效。取而代之的是,把Font對象從局部變量提供爲對象成員,在每次繪製窗口時重用同樣的對象:

private readonly Font _myFont =new Font( "Arial",10.0f );

protected override void OnPaint( PaintEventArgs e )
{
      e.Graphics.DrawString( DateTime.Now.ToString( ),_myFont, Brushes.Black, new PointF( 0,0 ));
      base.OnPaint( e );
}

 

8.所有的引用類型(值類型沒關係),即使是局部變量都分配在堆上。當垃圾回收器回收堆上的對象時候會花費而外的時間,隨着函數的退出,引用類型的局部變量就會變成垃圾。這裏強調的是在頻繁的調用的時候,如果調用的很少我們沒有必要把局部變量升級爲成員變量。

 

9.前面例子中使用的靜態屬性Brushes.Black,演示了另一個避免重複創建相似對象的技術。使用靜態成員變量來創建一些常用的引用類型的實例。考慮前面那個例子裏使用的黑色畫刷,每次當你要用黑色畫刷來畫一些東西時,你要在程序中創建和釋放大量的黑色畫刷。前面的一個解決方案就是在每個期望黑色畫刷的類中添加一個畫刷成員,但這還不夠。程序可能會創建大量的窗口和控件,這同樣會創建大量的黑色畫刷。.Net框架的設計者預知了這個問題,他們爲你創建一個簡單的黑色畫刷以便你在任何地方都可以重複使用。(也就是設計模式中的單例模式)

private static Brush _blackBrush;//類的靜態成員
public static Brush Black//靜態屬性
{
    get
     {
           if ( _blackBrush== null )//這裏也可以用於多線程的處理
           _blackBrush =new SolidBrush( Color.Black );
          return _blackBrush;
      }
}

 

10.System.String類就是一個恆定類型,在你創建一個字符串後,它的內容就不能更改了。當你編寫代碼來修改這些串的內容時,你實際上是創建了新的對象,並且讓舊的串成爲了垃圾。

string msg = "Hello, ";
msg += thisUser.Name;
msg += ". Today is ";
msg += System.DateTime.Now.ToString();

 字符串tmp1,tmp2,tmp3以及最原始的msg構造的(“Hello”),都成了垃圾。+=方法在字符串類上會生成一個新的對象並返回它。對於更多的複雜的字符串操作,你應該使用StringBuilter類。通過StringBuilter學到了一種思想:一些要經過多次構造後才能最終得到的對象,可以考慮使用一些對象生成器來簡化對象的創建。它提供了一個方法讓你的用戶來逐步的創建(你設計的)恆定類型,也用於維護這個類型。

 

 11.初始化常量類型通常有三種策略,選擇哪一種策略依賴於一個類型的複雜度。定義一組合適的構造器通常是最簡單的策略。也可以創建一個工廠方法(factory method)來進行初始化工作。這種方式對於創建一些常用的值比較方便。.NET框架中的Color類型就採用了這種策略來初始化系統顏色。例如,靜態 方法Color.FromKnownColor()和Color.FromName()可以根據一個指定的系統顏色名,來返回一個對應的顏色值。最後,對於需要多個步驟操作才能完整構造出一個常量類型的情況,我們可以通過創建一個可變的輔助類來解決。.NET中的String類就採用了這種策略,其輔助類爲System.Text.StringBuilder。我們可以使用StringBuilder類通過多步操作來創建一個String對象。


1.垃圾收集器(GC)控制着託管內存。但是我們需要對非託管代碼進行處理(數據庫連接,文件句柄,GDI+對象,COM對象等)

 

2.垃圾收集器是運行在一個單獨的線程中來移除程序中不再使用的內存。並且還會壓縮託管堆,使得空閒內存能集中在一個連續的區域。

 

3.對於非託管資源我們需要在類型中定義一個終結器,以此來確保釋放這些系統資源。在對象的內存被回收之前,系統會調用終結器,這樣我們可以在終結器中編寫我們的代碼來釋放該對象佔有的非託管資源。值得注意的是對象的終結器是在對象成爲垃圾之後調用的,但是在內存歸還之前,但是我們無法精確的控制“不再使用對象”和“執行終結器”這2個事件之間的關係。

 

4.當GC發現某個對象是垃圾但是需要終結器時,它還不能直接從內存上刪除這個對象。首先,它要調用終結器,但終結器的調用不是在垃圾回收器的同一個線程上運行的。取而代之的是,GC不得不把對象放置到終結器隊列中,讓另一個線程讓執行所有的終結器。GC繼續它自己的工作,從內存上移除其它的垃圾。在下一個GC回收時,那些被終結器了的對象纔會再從內存上移除。

 

5.Net回收器定義了一種稱爲“代齡”的機制來優化。代可以幫助GC來很快的 標識那些看上去看是垃圾的對象。所有從上一次GC回收後開始創建的對象稱爲第0代對象,所有那些經過一次GC回收後還存在的對象稱爲第1代對象。所有那些 經過2次或者2次以上GC回收後還存在的對象稱爲第2代對象。代齡的目的就是用來區分臨時變量以及一些應用程序的全局變量。第0代對象絕大部分是臨時的變 量。成員變量,以及一些全局變量很快會成爲第1代對象,最終成爲第2代對象。GC通過限制檢測第1以及第2代對象的頻度來優化它的工作。每一次GC循環都 檢測第0代對象,大概10次GC週期纔會檢查第0和第一代對象,大概100次GC週期才檢查一次第0,1,2代對象。讓我們考慮一下:一個須要終結器的對 象可能要比一個不用終結器的對象在內存裏多待上9個GC回收循環。如果它還沒有終結器,它將會移到第2代對象。在第2代對象中,一個可以生存上100個 GC循環直到下一個第2代集合。

 

6.記得一個垃圾回收器負責內存管理的託管環境的最大好處:內存泄漏,其它指針的相關的問題將不再存在。非內存資源迫使你要使用終結器來確保清理非內存資源。終結器會對你的應用程序性能產生一些影響,但你必須使用它們來防止資源泄漏。通過實現IDisposable接口並且使用它可以避免終結器在垃圾回收器上造成的性能損失。

Using Objects That Encapsulate Resources

When you write code that uses an object that encapsulates a resource, youshould make sure that the object'sDispose method gets called when youare finished using the object. You can do this with the C#usingstatement or by implementing a try/finally block in other languages that targetthe common language runtime.

C# Using Statement

The C# programming language's using statement makes a call to the Disposemethod more automatic, by simplifying the code that you must write to createand clean up an object. The using statement obtains one or moreresources, executes the statements that you specify, and then disposes of theobject. Note that the usingstatement is only useful for objects with alifetime that does not extend beyond the method in which the objects areconstructed. The following code example creates and cleans up an instance ofthe ResourceWrapperclass, as illustrated in the C# example of implementing aDispose method.

class myApp

{

  public static void Main()

   {

     using (ResourceWrapper r1 = new ResourceWrapper())

      {

        // Do something with the object.

        r1.DoSomething();

      }

   }

}

The preceding code, incorporating the using statement, isequivalent to the following.

class myApp

{

  public static void Main()

   {

     ResourceWrapper r1 = new ResourceWrapper();

     try

      {

        // Do something with the object.

        r1.DoSomething();

      }

     finally

      {

        // Check for a null resource.

        if (r1 != null)

        // Call the object's Dispose method.

        r1.Dispose();

      }

   }

}

The C# using statement allows you to acquire multiple resources ina single statement, which is equivalent internally to nested usingstatements. For more information and a code example, see using Statement(C# Reference).

Try/Finally Block

When you write managed code that uses an object that encapsulates aresource in languages other than C#, use a try/finally block to ensure that acall is made to the object's Dispose method. The following code examplecreates and cleans up an instance of the Resource class, as illustratedin the Visual Basic example ofimplementing aDispose method.

class myApp

  Public Shared Sub Main()

      Resource r1 = new Resource()

     Try

        ' Do something with the object.

        r1.DoSomething()

     Finally

        ' Check for a null resource.

        If Not (r1 is Nothing) Then

           ' Call the object's Dispose method.

           r1.Dispose()

        End If

     End Try

   EndSub

End Class


1.垃圾收集器(GC)控制着託管內存。但是我們需要對非託管代碼進行處理(數據庫連接,文件句柄,GDI+對象,COM對象等)

 

2.垃圾收集器是運行在一個單獨的線程中來移除程序中不再使用的內存。並且還會壓縮託管堆,使得空閒內存能集中在一個連續的區域。

 

3.對於非託管資源我們需要在類型中定義一個終結器,以此來確保釋放這些系統資源。在對象的內存被回收之前,系統會調用終結器,這樣我們可以在終結器中編寫我們的代碼來釋放該對象佔有的非託管資源。值得注意的是對象的終結器是在對象成爲垃圾之後調用的,但是在內存歸還之前,但是我們無法精確的控制“不再使用對象”和“執行終結器”這2個事件之間的關係。

 

4.當GC發現某個對象是垃圾但是需要終結器時,它還不能直接從內存上刪除這個對象。首先,它要調用終結器,但終結器的調用不是在垃圾回收器的同一個線程上運行的。取而代之的是,GC不得不把對象放置到終結器隊列中,讓另一個線程讓執行所有的終結器。GC繼續它自己的工作,從內存上移除其它的垃圾。在下一個GC回收時,那些被終結器了的對象纔會再從內存上移除。

 

5.Net回收器定義了一種稱爲“代齡”的機制來優化。代可以幫助GC來很快的 標識那些看上去看是垃圾的對象。所有從上一次GC回收後開始創建的對象稱爲第0代對象,所有那些經過一次GC回收後還存在的對象稱爲第1代對象。所有那些 經過2次或者2次以上GC回收後還存在的對象稱爲第2代對象。代齡的目的就是用來區分臨時變量以及一些應用程序的全局變量。第0代對象絕大部分是臨時的變 量。成員變量,以及一些全局變量很快會成爲第1代對象,最終成爲第2代對象。GC通過限制檢測第1以及第2代對象的頻度來優化它的工作。每一次GC循環都 檢測第0代對象,大概10次GC週期纔會檢查第0和第一代對象,大概100次GC週期才檢查一次第0,1,2代對象。讓我們考慮一下:一個須要終結器的對 象可能要比一個不用終結器的對象在內存裏多待上9個GC回收循環。如果它還沒有終結器,它將會移到第2代對象。在第2代對象中,一個可以生存上100個 GC循環直到下一個第2代集合。

 

6.記得一個垃圾回收器負責內存管理的託管環境的最大好處:內存泄漏,其它指針的相關的問題將不再存在。非內存資源迫使你要使用終結器來確保清理非內存資源。終結器會對你的應用程序性能產生一些影響,但你必須使用它們來防止資源泄漏。通過實現IDisposable接口並且使用它可以避免終結器在垃圾回收器上造成的性能損失。

1.初始化器(在聲明字段的時候就直接初始化,而不是在構造函數中處理)

   初始化生成的代碼會放置在類型的構造函數之前。初始化會在執行類型的基類的構造函數之前被執行,並且它們是按你聲明的先後關係順序執行的。

 

2.什麼情況下不應該初始化變量

  首先就是,如果你是初始化一個對象爲0,或者爲null。系統默認會在你任何代碼執行前,爲所有的內容都初始化爲0。系統置0的初始化是基於底層的CPU指令,對整個內存塊設置。你的任何其它置0的初始化語句是多餘的。C#編譯器忠實的添加額外的指令把內存設置爲0(多餘的設置)。這並沒有錯,只是效率不高(牽涉裝箱和拆箱)。事實上,如果是處理值類型數據,這是很不值的。

第二個低效率的是在你爲一個對象添加兩個構造函數時會產生。你使用初始化器初始化變量,而所有的構造函數也對這些變量進行了初始化。

public class MyClass
{
private ArrayList _coll = new ArrayList( );

MyClass( )
{
}

MyClass( int size )
{
    _coll = new ArrayList( size );
}
}

上面的代碼將會被編譯器編譯成下面的代碼 

public class MyClass
{
private ArrayList _coll;

MyClass( )
{
    _coll = new ArrayList( );
}

MyClass( int size )
{
    _coll = new ArrayList( );
    _coll = new ArrayList( size );
}
}

當你創建一個新的MyClass對象時,特別指定集合的大小,實際上你創建了兩個ArrayList數組列表。其中一個很快成爲垃圾對象。初始化器在所有的構造函數之前會執行,這樣它創建了一個ArrayList,之後構造函數會創建第2個ArrayList數組列表。

 

 3.你應該知道,在一個類型的任何實例初始化以前,你應該初始化它的靜態成員變量。在裏C#你可以使用靜態的初始化器和靜態構造函數來實現這個目 的。一個類的靜態構造函數是一個與衆不同的,它在所有的方法,變量或者屬性訪問前被執行。你可以用這個函數來初始化靜態成員變量,強制使用單件模式,或者實現其它任何在類型的實例可用前應該完成的工作。

  同樣,和實例的初始化器一樣,靜態的初始化器在靜態的構造函數調用前執行。並且,你的靜態初始化器在基類的靜態構造函數執行前被執行。

 

 4.構造第一個實例對象的操作順序

1、將所有的靜態變量設置我0。
2、執行靜態變量初始化器。
3、執行基類的靜態構造函數。
4、執行當前類型的靜態構造函數。
5、將所有的實例變量設置爲0。
6、執行實例變量初始化器。
7、執行適當的基類實例構造函數。
8、執行當前類型的實例構造函數。

 後續的同樣類型的實例從第5步開始,因爲類的初始化器只執行一次。另外,第6和第7步是可以被優化放置在構造器初始化器中,這樣編譯器便可以移除重複的指令。

系列二:資源管理(利用using,try/finally,減少內存垃圾)

1.我們常使用IDisposable接口的Dispose()方法來精確的釋放非託管系統資源。在日常代碼中我們確保調用Dispose()的方法是用using和try/catch/finally語句。

 

2.所有包含非託管資源的類型都會實現IDisposable接口,他們還會創建終結器,以防止我們忘記調用Dispose()。如果你忘記調用Dispose(),那些非內存資源會在晚些時候,終結器會調用時會釋放這些資源。這就使得這些對象在內存時待的時間更長,從而會使你的應用程序會因系統資源佔用太多而速度下降。

 

3.代碼展示:

public void ExecuteCommand( string connString,stringcommandString )
{
    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,stringcommandString )
{
     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,stringcommandString )
{
    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( );
}

 

4.我們不能對一個沒有實現IDispose接口的對象使用using方式,編譯器會報錯。

 

5.每一個using語句生成了一個新的嵌套的try/finally塊。我發現這是很糟糕的結構,所以,如果是遇到多個實現了IDisposable接口的對象時,我更願意寫自己的try/finally塊:

public void ExecuteCommand( string connString,stringcommandString )
{
     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();
        }
}

 

6.有一些對象同時支持Disponse和Close兩個方法來釋放資源。SqlConnection就是其中之一,你可以像這樣關閉

public void ExecuteCommand( string connString,stringcommandString )
{
      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,這個對象已經不再須要析構了(注:這個對象本身並沒有刪除出內存)。它是通過調用GC.SuppressFinalize()來實現這一點的。但Close()一般不會,因此調用了close()的對象仍然留在終結列表中,雖然這個時候不需要終結器的操作了。當你有選擇時,Dispose()比Colse()要好。

 

 7.我們應該對經常出現的局部變量提升爲類的成員變量(當在一個方法中經常調用一個變量時候,方法調用結束後,垃圾收集器就會去處理這個變量,每調用一次垃圾收集器就要增加額外的開銷),對那些是非託管資源的變量升級爲成員變量我們還應該把類設計成繼承IDisposable接口,在接口方法中釋放非託管資源。下面舉例說明:

 

protected override void OnPaint( PaintEventArgs e )
{
using ( Font MyFont = new Font( "Arial", 10.0f ))
{
    e.Graphics.DrawString( DateTime.Now.ToString(),
      MyFont, Brushes.Black, new PointF( 0,0 ));
}
base.OnPaint( e );
}

OnPaint()函數的調用很頻繁的,每次調用它的時候,都會生成另一個Font對象,而實際上它是完全一樣的內容。垃圾回收器每次都須要清理這些對象。這將是難以置信的低效。取而代之的是,把Font對象從局部變量提供爲對象成員,在每次繪製窗口時重用同樣的對象:

private readonly Font _myFont =new Font( "Arial",10.0f );

protected override void OnPaint( PaintEventArgs e )
{
      e.Graphics.DrawString( DateTime.Now.ToString( ),_myFont, Brushes.Black, new PointF( 0,0 ));
      base.OnPaint( e );
}

 

8.所有的引用類型(值類型沒關係),即使是局部變量都分配在堆上。當垃圾回收器回收堆上的對象時候會花費而外的時間,隨着函數的退出,引用類型的局部變量就會變成垃圾。這裏強調的是在頻繁的調用的時候,如果調用的很少我們沒有必要把局部變量升級爲成員變量。

 

9.前面例子中使用的靜態屬性Brushes.Black,演示了另一個避免重複創建相似對象的技術。使用靜態成員變量來創建一些常用的引用類型的實例。考慮前面那個例子裏使用的黑色畫刷,每次當你要用黑色畫刷來畫一些東西時,你要在程序中創建和釋放大量的黑色畫刷。前面的一個解決方案就是在每個期望黑色畫刷的類中添加一個畫刷成員,但這還不夠。程序可能會創建大量的窗口和控件,這同樣會創建大量的黑色畫刷。.Net框架的設計者預知了這個問題,他們爲你創建一個簡單的黑色畫刷以便你在任何地方都可以重複使用。(也就是設計模式中的單例模式)

private static Brush _blackBrush;//類的靜態成員
public static Brush Black//靜態屬性
{
    get
     {
           if ( _blackBrush== null )//這裏也可以用於多線程的處理
           _blackBrush =new SolidBrush( Color.Black );
          return _blackBrush;
      }
}

 

10.System.String類就是一個恆定類型,在你創建一個字符串後,它的內容就不能更改了。當你編寫代碼來修改這些串的內容時,你實際上是創建了新的對象,並且讓舊的串成爲了垃圾。

string msg = "Hello, ";
msg += thisUser.Name;
msg += ". Today is ";
msg += System.DateTime.Now.ToString();

 字符串tmp1,tmp2,tmp3以及最原始的msg構造的(“Hello”),都成了垃圾。+=方法在字符串類上會生成一個新的對象並返回它。對於更多的複雜的字符串操作,你應該使用StringBuilter類。通過StringBuilter學到了一種思想:一些要經過多次構造後才能最終得到的對象,可以考慮使用一些對象生成器來簡化對象的創建。它提供了一個方法讓你的用戶來逐步的創建(你設計的)恆定類型,也用於維護這個類型。

 

 11.初始化常量類型通常有三種策略,選擇哪一種策略依賴於一個類型的複雜度。定義一組合適的構造器通常是最簡單的策略。也可以創建一個工廠方法(factory method)來進行初始化工作。這種方式對於創建一些常用的值比較方便。.NET框架中的Color類型就採用了這種策略來初始化系統顏色。例如,靜態 方法Color.FromKnownColor()和Color.FromName()可以根據一個指定的系統顏色名,來返回一個對應的顏色值。最後,對於需要多個步驟操作才能完整構造出一個常量類型的情況,我們可以通過創建一個可變的輔助類來解決。.NET中的String類就採用了這種策略,其輔助類爲System.Text.StringBuilder。我們可以使用StringBuilder類通過多步操作來創建一個String對象。

系列二:資源管理(減少裝箱與拆箱,實現標準dispose)

1.值類型是數據的容器,它們不具備多態性。另一方面就是說,.Net框架被設計成單一繼承的引用類型,System.Object,在整個繼承關係中作爲根對象存在。設計這兩種類型的目的是截然不同的,.Net框架使用了裝箱與拆箱來鏈接兩種不同類型的數據。

 

2.裝箱是把一個值類型數據放置在一個無類型的引用對象上,從而使一個值類型在須要時可以當成引用類型來使用。拆箱則是額外的從“箱”上拷貝一份值類型數據。裝箱和拆箱可以讓你在須要使用System.Object對象的地方使用值類型數據。但裝箱與拆箱操作卻是性能的強盜,在些時候裝箱與拆箱會產生一些臨時對象,它會導致程序存在一些隱藏的BUG。應該儘可能的避免使用裝箱與拆箱。

 

3.裝箱可以把一個值類型數據轉化也一個引用類型,一個新的引用對象在堆上創建,它就是這個“箱子”,值類型的數據就在這個引用類型中存儲了一份拷貝。參見圖2.3,演示了裝箱的對象是如何訪問和存儲的。箱子中包含一份這個值類型對象的拷貝,並且複製實現了已經裝箱對象的接口。當你想從這個箱子中取回任何內容時,一個值類型數據的拷貝會被創建並返回。這就是裝箱與拆箱的關鍵性概念:對象的一個拷貝存放到箱子中,而不管何時你再訪問這個箱子時,另一個拷貝又會被創建。

4.Console.WriteLine("A few numbers:{0}, {1},{2}", 25, 32, 50);

 使用重載的Console.WriteLine函數須要一個System.Object類型的數組引用,整型是值類型,所以必須裝箱後才能傳給重載的WriteLine方法。唯一可以強制這三個整數成爲System.Object對象的方法就是把它們裝箱。另外,在WriteLine內部,通過調用箱子對象上的ToString()方法來到達箱子內部。某種意義上講,你生成了這樣的結構:

int i =25;
object o = i; // box
Console.WriteLine(o.ToString());

 在WriteLine內部,下面的執行了下面的代碼:

object o;
int i = ( int )o; // unbox
string output = i.ToString( );

  你可能自己從來不會寫這樣的代碼,但是,卻讓編譯器自動從一個指定的類型轉化爲System.Object,這確實是你做的。

爲了避免這麼挑剔的懲罰,在使用它們來調用WriteLine之前,你自己應該把你的類型轉化成字符串的實例。 console.WriteLine("A few numbers:{0}, {1},{2}",25.ToString(), 32.ToString(), 50.ToString());

  這段代碼使用已知的整數類型,而且值類型再也不會隱式的轉化爲System.Object類型。這個常見的例子展示了避免裝箱的第一個規則:注意隱式的轉化爲System.Object,如果可以避免,值類型不應該被System.Object代替。

 

5.

public struct Person
{
private string _Name;

public string Name
{
    get
    {      return_Name;    }
    set
    {      _Name =value;    }
}

public override string ToString( )
{    Return _Name;  }
}

ArrayList attendees = new ArrayList( );
Person p = new Person( "Old Name" );
attendees.Add( p );

Person p2 = (( Person )attendees[ 0 ] );
p2.Name = "New Name";

Console.WriteLine(attendees[ 0 ].ToString( ));

        Person是一個值類型數據,在存儲到ArrayList之前它被裝箱。這會產生一個拷貝。而在移出的Persone對象上通過訪問屬性做一些修改時,另一個拷貝被創建。而你所做的修改只是針對的拷貝,而實際上還有第三個拷貝通過ToString()方法來訪問attendees[0]中的對象。

        正因爲這以及其它一些原因,你應該創建一些恆定的值類型(參見條款7)。如果你非要在集合中使用可變的值類型,那就使用System.Array類,它是類型安全的。

        如果一個數組不是一個合理的集合,以C#1.x中你可以通過使用接口來修正這個錯誤。儘量選擇一些接口而不是公共的方法,來訪問箱子的內部去修改數據:

public interface IPersonName
{
string Name
{    get; set; }
}

struct Person : IPersonName
{
private string _Name;

public string Name
{
    get    {      return_Name;    }
    set    {      _Name= value;    }
}

public override string ToString( )
{
    return _Name;
}
}

ArrayList attendees = new ArrayList( );
Person p = new Person( "Old Name" );
attendees.Add( p ); // 裝箱

(( IPersonName )attendees[0 ] ).Name = "New Name";//這裏用的是接口的方法,接口是引用類型,不需要拆箱

Console.WriteLine(attendees[ 0 ].ToString( )); // 拆箱

 裝箱後的引用類型會實現原數據類型上所有已經實現的接口。這就是說,不用做拷貝,你可以通過調用箱子上的IPersonaName.Name方法來直接訪問請求到箱子內部的值類型數據。在值類型上創建的接口可以讓你訪問集合裏的箱子的內部,從而直接修改它的值。在值類型上實現的接口並沒有讓值類型成爲多態的,這又會引入裝箱的懲罰

我們應該對任何將值類型轉化成System.Object或者接口類型的構造保持密切的關注:把值類型放到集合裏,調用定義參數爲System.Object類型的方法,或者強制轉化爲System.Object。只要有可能,我們都應該避免這些構造!

 

6.討論如何寫代碼來管理這些類佔用的非內存資源了。一個標準的模式就是利用.Net框架提供的方法處理非內存資源。你的用戶也希望你遵守這個標準 的模式。也就是通過實現IDisposable接口來釋放非託管的資源,當然是在用戶記得調用它的時候,但如果用戶忘記了,析構函數也會被動的執行。它是和垃圾回收器一起工作的,確保在一些必要時候,你的對象只會受到因析構函數而造成的性能損失。這正是管理非託管資源的好方法,因此有必要徹底的弄明白它。

 

7.處在類繼承關係中頂層的基類應該實現IDisposable接口來釋放資源。這個類型也應該添加一個析構函數,做爲最後的被動機制。這兩個方法都應該是用虛方法來釋放資源,這樣可以讓它的派生類重載這個函數來釋放它們自己的資源。派生類只有在它自己須要釋放資源時才重載這個函數,並且一定要記得調用基類的虛方法。

 

 8. 開始時,如果你的類使用了非內存資源,則一定得有一個析構函數。你不能指望你的用戶總是記得調用Dispose方法,否則當他們忘記時,你會丟失一些資源

 

9.  當垃圾回收器運行時,它會直接從內存中移除不用析構的垃圾對象。而其它有析構函數的對象還保留在內存中。這些對象被添加到一個析構隊列中,垃圾回收器會起動一個線程專門來析構這些對象。當析構線程完成它的工作後,這些垃圾對象就可以從內存中移除了。就是說,須要析構的對象比不須要析構的對象 在內存中待的時間要長。但你沒得選擇。如果你是採用的這種被動模式,當你的類型佔用非託管資源時,你就必須寫一個析構函數。但目前你還不用擔心性能問題,下一步就保證你的用戶使用更加簡單,而且可以避免因爲析構函數而造成的性能損失。實現IDisposable接口是一個標準的模式來告訴用戶和進行時系統:你的對象佔有資源而且必須及時的釋放。IDisposable接口只有一個方法

public interface IDisposable
{
void Dispose( );
}

  實現IDisposable.Dispose()方法有責任完成下面的任務:
1、釋放所有的非託管資源。
2、釋放所有的託管資源(包括卸載一些事件)。
3、設置一個安全的標記來標識對象已經調用dispose()方法。如果在已經處理過的對象上調用任何方法時,你可以檢驗這個標記並且拋出一個ObjectDisposed的異常。
4、阻止析構。你要調用GC.SuppressFinalize(this)來完成最後的工作。

通過實現IDisposable接口,你寫成了兩件事:第一就是提供了一個機制來及時的釋放所有佔用的託管資源,另一個就是你提供了一個標準的模式讓用戶來釋放非託管資源。這是十分重要的,當你在你的類型上實現了IDisposable接口以後,用戶就可以避免析構時的損失。你的類就成了.Net社 區中表現相當良好的成員。

 

10.但在你創建的機制中還是存在一些漏洞。如何讓一個派生類清理自己的資源,同時還可以讓基類很好的再做資源清理呢?(譯註:因爲調用 Dispose方法時,必須調用基類的Dispose,當然是在基類有這個方法時。但前面說過,我們只有一個標記來標識對象是否處理過,不管先調用那個, 總得有一個方法不能處理這個標記,而這就存在隱患) 如果派生類重載了析構函數,或者自己添加實現了IDisposable接口,而這些方法又都是必須調用基類的方法的;否則,基類無法恰當的釋放資源。同樣,析構和處理共享了一些相同的職責:幾乎可以肯定你是複製了析構方法和處理方法之間的代碼。正如你會在原則26中學到的,重載接口的方法根本沒有如你所期望的那樣工作。Dispose標準模式中的第三個方法,通過一個受保護的輔助性虛函數,製造出它們的常規任務並且掛接到派生類來釋放資源。基類包含接口的核心代碼, 派生類提供的Dispose()虛函數或者析構函數來負責清理資源:

protected virtual void Dispose( bool isDisposing );

重載的方法Dispose(bool)同時完成析構和處理必須提供的任務,又因爲它是虛函數,它爲所有的派生類提供函數入口點。派生類可以重載這個函數,提供恰當的實現來釋放它自己的資源,並且調用基類的函數釋放基類資源。當isDisposing爲true時你可能同時清理託管資源和非託管資源, 當isDisposing爲false時你只能清理非託管資源。兩種情況下,都可以調用基類的Dispose(bool)方法讓它去清理它自己的資源。

        當你實現這樣的模式時,這裏有一個簡單的例子。MyResourceHog 類展示了IDisposable的實現,一個析構函數,並且創建了一個虛的Dispose方法:

public class MyResourceHog : IDisposable
{
// 標識是否調用過dispose()

private bool _alreadyDisposed = false;

// 終結器,調用虛方法
~MyResourceHog()
{
    Dispose( false );
}

// dispose方法,調用虛方法,終止析構函數
public void Dispose()
{
    Dispose( true );
    GC.SuppressFinalize( true );
}

// Dispose虛方法
protected virtual void Dispose( bool isDisposing )
{
    // 不釋放多次
    if ( _alreadyDisposed )
      return;
    if ( isDisposing )
    {
      // TODO: 釋放託管資源.
    }
    // TODO: 釋放非託管資源.
    // 設置是否已經執行dispose:
    _alreadyDisposed = true;
}
}

 

  如果派生類有另外的清理任務,就讓它實現Dispose方法:

public class DerivedResourceHog : MyResourceHog
{
// 設置自己的標識符.
private bool _disposed = false;

protected override void Dispose( bool isDisposing )
{
    // 不能多次調用.
    if ( _disposed )
      return;
    if ( isDisposing )
    {
      // TODO: 釋放自己的託管資源.
    }
    // TODO: 釋放非託管資源.

    // 讓基類釋放自己的資源.
    // 基類會調用 GC.SuppressFinalize( )處理析構函數
    base.Dispose( isDisposing );

    // 設置調用標識:
    _disposed = true;
}
}

 

注意,派生類和基類都有一個處理狀態的標記,這完全是防禦性的。重製的標記掩蓋了在處理時任何可能發生的錯誤,而且是單一的類型處理,而不是處理構 成這個對象的所有類型。你應該防禦性的寫處理方法dispose和析構函數,處理對象可能以任何順序發生,你可能會遇到這種情況:你的類中某個成員在你調 用Dispose方法以前已經被釋放過了。你沒有看到這種情況是因爲Dispose()方法是可以多次調用的。如果在一個已經被處理過的對象上調用該方法,就什麼也不發生。析構函數也有同樣的規則。任何對象的引用存在於內存中時,你不用檢測null引用。然而,你引用的對象可能已經處理掉了,或者它已經析構了。

        這就引入用了一個非常重要的忠告:對於任何與處理和資源清理相關的方法,你必須只釋放資源! 不要在處理過程中添加其它任何的任務。你在處理和清理中添加其它任務時,可能會在對象的生存期中遇到一些嚴重而繁雜的問題。對象在你創建它時出生,在垃圾回收器認領它時死亡。你可以認爲當你的程 序不能再訪問它們時,它們是睡眠的。你無法訪問對象,無法調用對象的方法。種種跡象表明,它們就像是死的。但對象在宣佈死亡前,析構函數還有最後一氣。析構函數什麼也不應該做,就是清理非託管資源。如果析構函數通過某些方法讓對象又變得可訪問,那麼它就復活了。(譯註:析構函數不是用戶調用的,也不由.Net系統調用,而是在由GC產生的額外線程上運行的) 它又活了,但這並不好。即使是它是從睡眼中喚醒的。這裏有一個明顯的例子:

public class BadClass
{
// 存儲全局對象引用:
private readonly ArrayList _finalizedList;
private string _msg;

public BadClass( ArrayList badList, string msg )
{
    // 對引用進行緩存

    _finalizedList = badList;
    _msg = (string)msg.Clone();
}

~BadClass()
{
    // 將對象加入 _finalizedList.
    // 對象又復活了,而不是垃圾
    _finalizedList.Add( this );
}
}

        當一個BadClass對象的析構函數執行時,它把自己的一個引用添加到了全局的鏈表中。這使得它自己又是可達的,它就又活了。前面向你介紹的這個方法會遇到一些讓人畏縮的難題。對象已經被析構了,所以垃圾回收器從此相信再也不用調用它的析構函數了。如果你實際要析構一個可達對象,這將不會成功。其次,你 的一些資源可能不再有用。GC不再從內存上移除那些只被析構隊列引用的對象,但它們可能已經析構了。如果是這樣,它們很可能已經不能使用了。(譯註:也就 是說利用上面的那個方法讓對象復活後,很有可能對象是不可用的。)儘管BadClass所擁有的成員還在內存裏,它們像是可以被析構或者處理,但C#語言沒有一個方法可以讓你控制析構的次序,你不能讓這樣的結構可靠的運行。不要嘗試。

        我還沒有看到這樣的代碼:用這樣明顯的方式來複活一個對象,除非是學術上的練習。但我看過這樣的代碼,析構函數試圖完成一些實質的工作,最後還通過析構函數的調用把引用放到對象中,從而把自己復活。析構函數裏面的代碼看上去是精心設計的,另外還有處理函數裏的。再檢查一遍,這些代碼是做了其它事情,而不是 釋放資源!這些行爲會爲你的應用程序在後期的運行中產生很多BUG。刪除這些方法,確保析構函數和Dispose()方法除了清理資源外,什麼也不做。

        在託管環境裏,我們不需要爲每一個類型都要創建一個終結器。只有當類型包含非託管類型,或者類所包含的成員實現了IDisposable接口,我們才需要爲它們創建終結器。即使我們只需要IDisposable接口(而不需要終結器),我們也應該實現完整的模式。否則,派生類在實現標準Dispose模式時將變得非常複雜。我們應該遵循上述的Dispose模式,這會使得包括我們自己、我們類型的用戶以及由我們的類型創建派生類的開發人員的生活變得更加輕鬆。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章