當你拋出異常時,你就在應用程序中引入了一個中斷事件。而且危機到程序 的控制流程。使得期望的行為不能發生。更糟糕的是,你還要把清理工作留給最 終寫代碼捕獲了異常的程序員。而當一個異常發生時,如果你可以從你所管理的 程序狀態中直接捕獲,那麼你還可以采取一些有效的方法。謝天謝地,C#社區不 須要創建自己的異常安全策略,C++社區裡的人已經為我們完成了所有的艱巨的 工作。以Tom Cargill的文章開頭:“異常處理:一種錯誤的安全感覺, ” 而且Herb Sutter,Scott Meyers,Matt Austern,Greg Colvin和Dave Abrahams也在後繼寫到這些。C++社區裡的大量已經成熟的實踐可以應用在C#應 用程序中。關於異常處理的討論,一直持續了6年,從1994年到2000年。他們討論,爭論,以及驗證很多解決困難問題的方法。我們應該在C#裡利用所有這些艱 難的工作。
Dave Abrahams定義了三種安全異常來保證程序:基本保護, 強保證,以及無拋出保證。Herb Sutter在他的Exceptional C++(Addison- Wesley, 2000)一書討論了這些保證。基本保證狀態是指沒有資源洩漏,而且所 有的對象在你的應用程序拋出異常後是可用的。強異常保證是創建在基本保證之 上的,而且添加了一個條件,就是在異常拋出後,程序的狀態不發生改變。無拋 出保證狀態是操作決對不發生失敗,也就是從在某個操作後決不會發生異常。強 異常保證在從異常中恢復和最簡單異常之間最平衡的一個。基本保證一般在 是.Net和C#裡以默認形式發生。運行環境處理托管內存。只有在一種情況下,你 可能會有資源洩漏,那就是在異常拋出時,你的程序占有一個實現了 IDisposable接口的資源對象。原則18解釋了如何在對面異常時避免資源洩漏。
強異常保證狀態是指,如果一個操作因為某個異常中斷,程序維持原狀 態不改變。不管操作是否完成,都不修改程序的狀態,這裡沒有折衷。強異常保 證的好處是,你可以在捕獲異常後更簡單的繼續執行程序,當然也是在你遵守了 強異常保證情況下。任何時候你捕獲了一個異常,不管操作意圖是否已經發生, 它都不應該開始了,而且也不應該做任何修改。這個狀態就像是你還沒有開始這 個操作行為一樣。
很多我所推薦的方法,可以更簡單的幫助你來確保進 行強異常保證。你程序使用的數據元素應該存為一個恆定的類型(參見原則6和原 則7)。如果你組並這兩個原則,對程序狀態進行的任何修改都可以在任何可能引 發異常的操作完成後簡單的發生。常規的原則是讓任何數據的修改都遵守下面的 原則:
1、對可能要修改的數據進行被動式的拷貝。
2、在拷貝的 數據上完成修改操作。這包括任何可能異常異常的操作。
3、把臨時的拷 貝數據與源數據進行交換。 這個操作決不能發生任何異常。
做為一個例 子,下面的代碼用被動的拷貝方式更新了一個雇員的標題和工資 :
public void PhysicalMove( string title, decimal newPay )
{
// Payroll data is a struct:
// ctor will throw an exception if fields aren't valid.
PayrollData d = new PayrollData( title, newPay,
this.payrollData.DateOfHire );
// if d was constructed properly, swap:
this.payrollData = d;
}
有些時候,這種強保證只是效率很低而不被支持,而 且有些時候,你不能支持不發生潛在BUG的強保證。開始的那個也是最簡單的那 個例子是一個循環構造。當上面的代碼在一個循環裡,而這個循環裡有可能引發 程序異常的修改,這時你就面臨一個困難的選擇:你要麼對循環裡的所有對象進 行拷貝,或者降低異常保證,只對基本保證提供支持。這裡沒有固定的或者更好 的規則,但在托管環境裡拷貝堆上分配的對象,並不是像在本地環境上那開銷昂 貴。在.Net裡,大量的時間都花在了內存優化上。我喜歡選擇支持強異常保證, 即使這意味要拷貝一個大的容器:獲得從錯誤中恢復的能力,比避免拷貝獲得小 的性能要劃算得多。在特殊情況下,不要做無意義的拷貝。如果某個異常在任何 情況下都要終止程序,這就沒有意義做強異常保證了。我們更關心的是交換引用 類型數據會讓程序產生錯誤。考慮這個例子:
private DataSet _data;
public IListSource MyCollection
{
get
{
return _data;
}
}
public void UpdateData( )
{
// make the defensive copy:
DataSet tmp = _data.Clone( ) as DataSet;
using ( SqlConnection myConnection =
new SqlConnection( connString ))
{
myConnection.Open();
SqlDataAdapter ad = new SqlDataAdapter( commandString,
myConnection );
// Store data in the copy
ad.Fill( tmp );
// it worked, make the swap:
_data = tmp;
}
}
這看上去很不 錯,使用了被動式的拷貝機制。你創建了一個DataSet的拷貝,然後你就從數據 庫裡攫取數據來填充臨時的DataSet。最後,把臨時存儲交換回來。這看上去很 好,如果在取回數據中發生了任何錯誤,你就相當於沒有做任何修改。
這只有一個問題:它不工作。MyCollection屬性返回的是一個對_data對象的引 用(參見原則23)。所有的類的使用客戶,在你調用了UpdateData後,還是保持著 原原來數據的引用。他們所看到的是舊數據的視圖。交換的伎倆在引用類型上不 工作,它只能在值類型上工作。因為這是一個常用的操作,對於DataSets有一個 特殊的修改方法:使用Merge 方法:
private DataSet _data;
public IListSource MyCollection
{
get
{
return _data;
}
}
public void UpdateData( )
{
// make the defensive copy:
DataSet tmp = new DataSet( );
using ( SqlConnection myConnection =
new SqlConnection( connString ))
{
myConnection.Open();
SqlDataAdapter ad = new SqlDataAdapter( commandString,
myConnection);
ad.Fill( tmp );
// it worked, merge:
_data.Merge( tmp );
}
}
合並 修改到當前的DataSet上,就讓所有的用戶保持可用的引用,而且內部的DataSet 內容已經更新。
在一般情況下,你不能修正像這樣的引用類型交換,然 後還想確保用戶擁有當前的對象拷貝。交換工作只對值類型有效,如果你遵守原 則6,這應該是足夠了。
最後,也是最嚴格的,就是無拋出保證。無拋出 保證聽起來很優美,就像是:一個方法是無拋出保證,如果它保證總是完成任務 ,而且不會在方法裡發生任何異常。在大型程序中,對於所有的常規問題並不是 實用的。然而,如果在一個小的范圍上,方法必須強制無拋出保證。析構和處理 方法就必須保證無異常拋出。在這兩種情況下,拋出任何異常會引發更多的問題 ,還不如做其它的選擇。在析構時,拋出異常中止程序就不能進一步的做清理工 作了。
如果在處理方法中拋出異常,系統現在可能有兩個異常在運行系 統中。.Net環境丟失前面的一個異常然後拋出一個新的異常。你不能在程序的任 何地方捕獲初始的異常,它被系統干掉了。這樣,你又如何能從你看不見的錯誤 中恢復呢?
最後一個要做無拋出保證的地方是在委托對象上。當一個委 托目標拋出異常時,在這個多播委托上的其它目標就不能被調用了。對於這個問 題的唯一方法就是確保你在委托目標上不拋出任何異常(譯注:我不造成這種做 法,而且誰又能保證在委托目標上不 出現異常呢?)。讓我們再重申一下:委托 目標(包括事件句柄)應該不拋出異常。這樣做就意味關引發事件的代碼不應該參 與到強異常保證中。但在這裡,我將要修改這一建議。原則21告訴你可以調用委 托,因此你可以從一個異常中恢復。想也想得到,並不是每個人都這樣做,所以 你應該在委托句柄上拋出異常。只在要你在委托上不拋出異常,並不意味著其它 人也遵守這一建議,在你自己的委托調用上,不能指望它是無拋出的保證。這主 是被動式程序設計:你應該盡可能做原最好,因為其他程序可能做了他們能做的 最壞的事。
異常列在應用程序的控制流程上引發了一系列的改變,在最 糟糕的情況下,任何事情都有可能發生,或者任何事情也有可能不發生。在異常 發生時,唯一可以知道哪些事情發生,哪些事情沒有發生的方法就是強制強異常 保證。當一個操作不管是完成還是沒有完成時都不做任何修改。構造和Dispose ()以及委托目標是特殊的情況,而且它們應該在不充許任何異常逃出環境的情況 下完成任務。最後一句話:小心對引用類型的交換,它可能會引發大量潛在的 BUG。
返回教程目錄