Effective C#原則:調(diào)用Dispose()方法
Effective C#原則(一)
使用非托管資源的類型必須實(shí)現(xiàn)IDisposable接口的Dispose()方法來(lái)精確的釋放系統(tǒng)資源。.Net環(huán)境的這一規(guī)則使得釋放資源代碼的職責(zé)是類型的使用者,而不是類型或系統(tǒng)。因此,任何時(shí)候你在調(diào)用Dispose()方法的類型時(shí),你就有責(zé)任來(lái)調(diào)用Dispose()方法來(lái)釋放資源。最好的方法來(lái)保證Dispose()被調(diào)用的結(jié)構(gòu)是使用using語(yǔ)句或者try/finally塊。
所有包含非托管資源的類型應(yīng)該實(shí)現(xiàn)IDisposable接口,另外,當(dāng)你忘記恰當(dāng)?shù)奶幚磉@些類型時(shí),它們會(huì)被動(dòng)的創(chuàng)建析構(gòu)函數(shù)。如果你忘記處理這些對(duì)象,那些非內(nèi)存資源會(huì)在晚些時(shí)候,析構(gòu)函數(shù)被確切調(diào)用時(shí)得到釋放。這就使得這些對(duì)象在內(nèi)存時(shí)待的時(shí)間更長(zhǎng),從而會(huì)使你的應(yīng)用程序會(huì)因系統(tǒng)資源占用太多而速度下降。
幸運(yùn)的是,C#語(yǔ)言的設(shè)計(jì)者精確的釋放資源是一個(gè)常見的任務(wù)。他們添加了一個(gè)關(guān)鍵字來(lái)使這變得簡(jiǎn)單了。
假設(shè)你寫了下面的代碼:
- public void ExecuteCommand( string connString, string commandString )
- {
- SqlConnection myConnection = new SqlConnection( connString );
- SqlCommand mySqlCommand = new SqlCommand( commandString,
- myConnection );
- myConnection.Open();
- mySqlCommand.ExecuteNonQuery();
- }
這個(gè)例子中的兩個(gè)可處理對(duì)象沒有被恰當(dāng)?shù)尼尫牛篠qlConnection和SqlCommand。兩個(gè)對(duì)象同時(shí)保存在內(nèi)存里直到析構(gòu)函數(shù)被調(diào)用。(這兩個(gè)類都是從System.ComponentModel.Component繼承來(lái)的。)
解決這個(gè)問題的方法就是在使用完命令和鏈接后就調(diào)用它們的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( );
- }
Effective C#原則(二)
這很好,除非SQL命令在執(zhí)行時(shí)拋出異常,這時(shí)你的Dispose()調(diào)用就永遠(yuǎn)不會(huì)成功。using語(yǔ)句可以調(diào)用Dispose()方法。當(dāng)你把對(duì)象分配到using語(yǔ)句內(nèi)時(shí),C#的編譯器就把這些對(duì)象放到一個(gè)try/finally塊內(nèi):
- public void ExecuteCommand( string connString, string commandString )
- {
- using ( SqlConnection myConnection = new SqlConnection( connString ))
- {
- using ( SqlCommand mySqlCommand = new SqlCommand( commandString, myConnection ))
- {
- myConnection.Open();
- mySqlCommand.ExecuteNonQuery();
- }
- }
- }
當(dāng)你在一個(gè)函數(shù)內(nèi)使用一個(gè)可處理對(duì)象時(shí),using語(yǔ)句是最簡(jiǎn)單的方法來(lái)保證這個(gè)對(duì)象被恰當(dāng)?shù)奶幚淼?。?dāng)這些對(duì)象被分配時(shí),會(huì)被編譯器放到一個(gè)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( );
- }
(譯注:就我個(gè)人對(duì)try/catch/finally塊的使用經(jīng)驗(yàn)而言,我覺得上面這樣的做法非常不方便??梢员WC資源得到釋放,卻無(wú)法發(fā)現(xiàn)錯(cuò)誤。關(guān)于如何同時(shí)拋出異常又釋放資源的方法可以參考一下其它相關(guān)資源,如Jeffrey的.Net框架程序設(shè)計(jì),修訂版)
如果你把一個(gè)不能處理類型的變量放置在using語(yǔ)句內(nèi),C#編譯器給出一個(gè)錯(cuò)誤,例如:
- // Does not compile:
- // String is sealed, and does not support IDisposable.
- using( string msg = "This is a message" )
- Console.WriteLine( msg );
using只能在編譯時(shí),那些支持IDispose接口的類型可以使用,并不是任意的對(duì)象:
- // Does not compile.
- // Object does not support IDisposable.
- using ( object obj = Factory.CreateResource( ))
- Console.WriteLine( obj.ToString( ));
如果obj實(shí)現(xiàn)了IDispose接口,那么using語(yǔ)句就會(huì)生成資源清理代碼,如果不是,using就退化成使用using(null),這是安全的,但沒有任何作用。如果你對(duì)一個(gè)對(duì)象是否應(yīng)該放在using語(yǔ)句中不是很確定,寧可為了更安全:假設(shè)要這樣做,而且按前面的方法把它放到using語(yǔ)句中。
Effective C#原則(三)
這里講了一個(gè)簡(jiǎn)單的情況:無(wú)論何時(shí),當(dāng)你在某個(gè)方法內(nèi)使用一個(gè)可處理對(duì)象時(shí),把這個(gè)對(duì)象放在using語(yǔ)句內(nèi)?,F(xiàn)在你學(xué)習(xí)一些更復(fù)雜的應(yīng)用。還是前面那個(gè)例子里須要釋放的兩個(gè)對(duì)象:連接對(duì)象和命令對(duì)象。前面的例子告訴你創(chuàng)建了兩個(gè)不同的using語(yǔ)句,一個(gè)包含一個(gè)可處理對(duì)象。每個(gè)using語(yǔ)句就生成了一個(gè)不同的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( );
- }
- }
每一個(gè)using語(yǔ)句生成了一個(gè)新的嵌套的try/finally塊。我發(fā)現(xiàn)這是很糟糕的結(jié)構(gòu),所以,如果是遇到多個(gè)實(shí)現(xiàn)了IDisposable接口的對(duì)象時(shí),我更愿意寫自己的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();
- }
- }
(譯注:作者里的判斷對(duì)象是否為null是很重要的,特別是一些封裝了COM的對(duì)象,有些時(shí)候的釋放是隱式的,當(dāng)你再釋放一些空對(duì)象時(shí)會(huì)出現(xiàn)異常。例如:同一個(gè)COM被兩個(gè)不同接口的變量引用時(shí),在其中一個(gè)上調(diào)用了Dispose后,另一個(gè)的調(diào)用就會(huì)失敗。在.Net里也要注意這樣的問題,所以要判斷對(duì)象是否為null)
然而,請(qǐng)不要自作聰明試圖用as來(lái)寫這樣的using語(yǔ)句:
- 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();
- }
- }
這看上去很清爽,但有一個(gè)狡猾的(subtle )的bug。 如果SqlCommand()的構(gòu)造函數(shù)拋出異常,那么SqlConnection對(duì)象就不可能被處理了。你必須確保每一個(gè)實(shí)現(xiàn)了IDispose接口的對(duì)象分配在在using范圍內(nèi),或者在try/finally塊內(nèi)。否則會(huì)出現(xiàn)資源泄漏。
目前為止,你已經(jīng)學(xué)會(huì)了兩種最常見的情況。無(wú)論何時(shí)在一個(gè)方法內(nèi)處理一個(gè)對(duì)象時(shí),使用using語(yǔ)句是最好的方法來(lái)確保申請(qǐng)的資源在各種情況下都得到釋放。當(dāng)你在一個(gè)方法里分配了多個(gè)(實(shí)現(xiàn)了IDisposable接口的)對(duì)象時(shí),創(chuàng)建多個(gè)using塊或者使用你自己的try/finally塊。
對(duì)可處理對(duì)象的理解有一點(diǎn)點(diǎn)細(xì)微的區(qū)別。有一些對(duì)象同時(shí)支持Disponse和Close兩個(gè)方法來(lái)釋放資源。SqlConnection就是其中之一,你可以像這樣關(guān)閉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();
- }
- }
這個(gè)版本關(guān)閉了鏈接,但它確實(shí)與處理對(duì)象是不一樣的。Dispose方法會(huì)釋放更多的資源,它還會(huì)告訴GC,這個(gè)對(duì)象已經(jīng)不再須要析構(gòu)了(譯注:關(guān)于C#里的析構(gòu),可以參考其它方面的書籍)。調(diào)用Dispose()方法的GC.SuppressFinalize(),但Close()一般不會(huì)。結(jié)果就是,對(duì)象會(huì)到析構(gòu)隊(duì)列中排隊(duì),即使析構(gòu)并不是須要的。當(dāng)你有選擇時(shí),Dispose()比Colse()要好。你會(huì)在原則18里學(xué)習(xí)更更精彩的內(nèi)容。
Dispose()并不會(huì)從內(nèi)存里把對(duì)象移走,對(duì)于讓對(duì)象釋放非托管資源來(lái)說(shuō)是一個(gè)hook。這就是說(shuō)你可能遇到這樣的難題,就是釋放一個(gè)還在使用的對(duì)象。不要釋放一個(gè)在程序其它地方還在引用的對(duì)象。
在某些情況下,C#里的資源管理比C++還要困難。你不能指望確定的析構(gòu)函數(shù)來(lái)清理你所使用的所有資源。但垃圾回收器卻讓你更輕松,你的大從數(shù)類型不必實(shí)現(xiàn)IDisposable接口。在.Net框架里的1500多個(gè)類中,只有不到100個(gè)類實(shí)現(xiàn)了IDisposable接口。當(dāng)你使用一個(gè)實(shí)現(xiàn)了IDisposeable接口的對(duì)象時(shí),記得在所有的類里都要處理它們。你應(yīng)該把它們包含在using語(yǔ)句中,或者try/finally塊中。不管用哪一種,請(qǐng)確保每時(shí)每刻對(duì)象都得到了正確的釋放。
【編輯推薦】