天天看點

Effective C#之Item 45:Prefer the Strong Exception Guarantee

  rel="File-List" href="file:///C:%5CDOCUME%7E1%5CHelios%5CLOCALS%7E1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_filelist.xml" target="_blank" rel="external nofollow" > rel="themeData" href="file:///C:%5CDOCUME%7E1%5CHelios%5CLOCALS%7E1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_themedata.thmx" target="_blank" rel="external nofollow" > rel="colorSchemeMapping" href="file:///C:%5CDOCUME%7E1%5CHelios%5CLOCALS%7E1%5CTemp%5Cmsohtmlclip1%5C01%5Cclip_colorschememapping.xml" target="_blank" rel="external nofollow" >

Chapter 6. Miscellaneous 第六章 雜項

Some items don't fit convenient categories. But that does not limit their importance. Understanding code access security is important for everyone, as is understanding exception-handling strategies. Other recommendations are constantly changing because C# is a living language, with an active community and an evolving standard. It pays to look forward to these changes and prepare for them; they will impact your future work.

一些條款不适合任何目錄,但是卻不限制它們的重要性。了解代碼通路安全性對每個人來說是很重要的,就像了解異常處理政策一樣。其他的建議是在不斷的變化的,因為C#是個在不斷成長的語言,伴随着不斷的交流和進化标準。提前對待這些變化并做準備,它們會影響你未來的工作。

Item 45: Prefer the Strong Exception Guarantee 優先選擇強異常保障

When you throw an exception, you've introduced a disruptive event into the application. Control flow has been compromised. Expected actions did not occur. Worse, you've left the cleanup operation to the programmer writing the code that eventually catches the exception. The actions available when you catch exceptions are directly related to how well you manage program state when an exception gets thrown. Thankfully, the C# community does not need to create its own strategies for exception safety; the C++ community did all the hard work for us. Starting with Tom Cargill's article "Exception Handling: A False Sense of Security," and continuing with writings by Herb Sutter, Scott Meyers, Matt Austern, Greg Colvin, and Dave Abrahams, the C++ community developed a series of best practices that we can adapt to C# applications. The discussions on exception handling occurred over the course of 6 years, from 1994 to 2000. They discussed, debated, and examined many twists on a difficult problem. We should leverage all that hard work in C#.

當你抛出異常時,就給應用程式引入了一個分裂性的事件。控制流已經被折中了。期望的行為并沒有發生。更糟糕的是,你将清理操作留給了編寫代碼的程式員,他們逐漸的捕捉異常。當你在捕捉異常時采取的有效行動都直接和異常抛出時你将程式狀态管理的如何相關。謝天謝地,C#社群不需要建立自己的異常安全政策;C++社群為我們做了所有艱難的工作。從Tom Cargill's的文章"Exception Handling: A False Sense of Security,"開始,Herb Sutter, Scott Meyers, Matt Austern, Greg Colvin和 Dave Abrahams也相繼寫到這些,C++社群開發出一系列的很好實踐,我們可以将其改造到C#應用程式中。關于異常處理的讨論,一直持續了6年,從1994到2000。他們不斷的讨論,争吵,在某個困難的問題上檢查很多解決方法。在C#裡面我們要利用所有這些艱難的工作。

Dave Abrahams defined three exception-safe guarantees: the basic guarantee, the strong guarantee, and the no-throw guarantee. Herb Sutter discussed these guarantees in his book Exceptional C++ (Addison-Wesley, 2000). The basic guarantee states that no resources are leaked and all objects are in a valid state after your application throws an exception. The strong exception guarantee builds on the basic guarantee and adds that if an exception occurs, the program state did not change. The no-throw guarantee states that an operation never fails, from which it follows that a method does not ever throw exceptions. The strong exception guarantee provides the best trade-off between recovering from exceptions and simplifying exception handling.

Dave Abrahams定義了三個異常安全的保障機制:基本保障,強保障,無抛出保障。Herb Sutter在他的書籍《Exceptional C++ (Addison-Wesley, 2000)》裡面讨論了這3個保障。基本保障表述了:沒有資源會被洩露,在應用程式抛出異常後,所有的對象都處于有效狀态。強異常保障建構在基本保障的基礎上,并有所增加:如果發生了異常,程式狀态不能改變。無抛出保障表述了:一個操作從不會失敗,即,一個方法從不會抛出異常。強保障在由異常中恢複以及簡化異常處理之間,提供了最好的折中。

The basic guarantee happens almost by default in .NET and C#. The environment handles memory management. The only way you can leak resources due to exceptions is to throw an exception while you own a resource that implements IDisposable. Item 18 explains how to avoid leaking resources in the face of exceptions.

在.Net和C#裡面,基本保障幾乎是預設的發生。環境處理了記憶體管理。你唯一可以通過異常洩露資源的方式就是當你擁有一個實作了IDisposable接口的資源抛出異常時。Item 18解釋了在面對異常時如何避免洩露資源。

The strong guarantee states that if an operation terminates because of an exception, program state remains unchanged. Either an operation completes or it does not modify program state; there is no middle ground. The advantage of the strong guarantee is that you can more easily continue execution after catching an exception when the strong guarantee is followed. Anytime you catch an exception, whatever operation was attempted did not occur. It did not start, and it did not make some changes. The state of the program is as though you did not start the action.

強保障表述了:如果一個操作因為異常而終止的話,程式狀态要保持不變。要麼操作要完成,要麼就不修改程式的狀态;沒有中間狀态。強保障的優勢在于,如果你遵守的話,在捕獲異常後,你可以更容易的繼續執行下去。任何時間你捕獲一個異常,不管任何想要執行的操作都不會發生。它不會再開始,不做任何修改。程式的狀态就像你什麼也沒做過一樣。

Many of the recommendations I made earlier will help ensure that you meet the strong exception guarantee. Data elements that your program uses should be stored in immutable value types (see Items 6 and 7). If you combine those two items, any modification to program state can easily take place after performing any operation that might throw an exception. The general guideline is to perform any data modifications in the following manner:

前面我給出的很多建議都将幫助保證你滿足強異常保障。你的程式使用的資料元素應該存儲在不可變值類型中(見 Item6和Item7)。如果你合并這2項,那麼在執行任何可能抛出異常的操作後,任何對程式狀态做得修改都可以簡單的發生。正常的原則是讓任何資料的修改都遵守下面的原則:

Make defensive copies of data that will be modified.

Perform any modifications to these defensive copies of the data. This includes any operations that might throw an exception.

Swap the temporary copies back to the original. This operation cannot throw an exception.

對将要被修改的資料進行防禦性拷貝。

在資料的防禦性拷貝進行任何修改,包含任何可能抛出異常的操作。

将臨時的拷貝資料與中繼資料進行交換,該錯誤不會抛出異常。

As an example, the following code updates an employee's title and pay using defensive copy:

作為一個例子,下面的代碼,使用防禦性拷貝的方式更新了雇員的标題和薪酬:

  1. public void PhysicalMove( string title, decimal newPay )
  2. {
  3.   // Payroll data is a struct:
  4.   // ctor will throw an exception if fields aren't valid.
  5.   PayrollData d = new PayrollData( title, newPay, this.payrollData.DateOfHire );
  6.   // if d was constructed properly, swap:
  7.   this.payrollData = d;
  8. }

Sometimes, the strong guarantee is just too inefficient to support, and sometimes you cannot support the strong guarantee without introducing subtle bugs. The first and simplest case is looping constructs. When the code inside a loop modifies the state of the program and might throw an exception, you are faced with a tough choice: You can either create a defensive copy of all the objects used in the loop, or you can lower your expectations and support only the basic exception guarantee. There are no hard and fast rules, but copying heap-allocated objects in a managed environment is not as expensive as it was in native environments. A lot of time has been spent optimizing memory management in .NET. I prefer to support the strong exception guarantee whenever possible, even if it means copying a large container: The capability to recover from errors outweighs the small performance gain from avoiding the copy. In special cases, it doesn't make sense to create the copy. If any exceptions would result in terminating the program anyway, it makes no sense to worry about the strong exception guarantee. The larger concern is that swapping reference types can lead to program errors. Consider this example:

有時,要支援強保障的話效率太低了;有時,不引入一些小bug的話,也難支援強保障。第一個也是最簡單的情況是循環結構體。當一個循環内部的代碼修改程式狀态并且可能抛出異常時,你将面對一個艱難的選擇:你可以對循環裡面使用的所有的對象都建立防禦性拷貝;或者可以降低你的期望,僅僅支援基本異常保障。沒有固定的或者更快的規則,在托管環境下對基于堆配置設定的對象進行拷貝并不像在原始環境下一樣代價昂貴。.Net裡面花費很多時間來優化記憶體管理。隻要可能,我就更願意支援強類型保障,哪怕意味着拷貝一個大的容器:從錯誤中恢複的能力比避免拷貝而獲得的小性能要重要。在特殊情況下,建立拷貝是講不通的。如果任何異常都會到這程式的終止的話,就沒有必要擔心強異常保障了。交換引用類型會一起程式錯誤,這将是更大的關注點。考慮這個例子:

  1. private DataSet _data;
  2. public IListSource MyCollection
  3. {
  4.   get
  5.   {
  6.     return _data;
  7.   }
  8. }
  9. public void UpdateData( )
  10. {
  11.   // make the defensive copy:
  12.   DataSet tmp = _data.Clone( ) as DataSet;
  13.   using ( SqlConnection myConnection = new SqlConnection( connString ))
  14.   {
  15.     myConnection.Open();
  16.     SqlDataAdapter ad = new SqlDataAdapter( commandString, myConnection );
  17.     // Store data in the copy
  18.     ad.Fill( tmp );
  19.     // it worked, make the swap:
  20.     _data = tmp;
  21.   }
  22. }

This looks like a great use of the defensive copy mechanism. You've created a copy of the DataSet. Then you grab new data from the database and fill the temporary DataSet. Finally, you swap the temporary storage back. It looks great. If anything goes wrong trying to retrieve the data, you have not made any changes.

這看起來是充分利用了防禦性拷貝的機制。你建立了一個DataSet的拷貝,然後從資料庫裡面得到新的資料,填充到臨時的DataSet裡,最後,對臨時存儲空間進行交換。看起來很好。如果嘗試獲得資料的任何嘗試失敗了,你将不做任何改變。

There's only one problem: It doesn't work. The MyCollection property returns a reference to the _data object (see Item 23). All the clients of this class are left holding references to the original DataSet after you call UpdateData. They are looking at the old view of the data. The swap trick does not work for reference types it works only for value types. Because it is a common operation, there is a specific fix for DataSets. Use the Merge method:

僅僅有一個問題:上面的代碼不能工作。MyCollection屬性傳回了一個對_data對象的引用(見Item23)。在你調用UpdateData之後,該類的所有使用者都擁有對原來DataSet的引用。他們看到的是原來的資料。對于引用類型,交換過程開了個玩笑;交換隻對值類型有用。因為這是一個通用的操作,是以對于DataSets有一種特定的修複方法。使用Merge方法:

  1. private DataSet _data;
  2. public IListSource MyCollection
  3. {
  4.   get
  5.   {
  6.     return _data;
  7.   }
  8. }
  9. public void UpdateData( )
  10. {
  11.   // make the defensive copy:
  12.   DataSet tmp = new DataSet( );
  13.   using ( SqlConnection myConnection = new SqlConnection( connString ))
  14.   {
  15.     myConnection.Open();
  16.     SqlDataAdapter ad = new SqlDataAdapter( commandString, myConnection);
  17.     ad.Fill( tmp );
  18.     // it worked, merge:
  19.     _data.Merge( tmp );
  20.   }
  21. }

Merging the changes into the current DataSet lets all clients keep a valid reference, and the internal contents of the DataSet are updated.

将改變合并到目前的DataSet,讓所有的客戶都保持有效的引用,DataSet的内部内容也得到了更新。

In the general case, you cannot fix the problem of swapping reference types while still ensuring that all clients have the current copy of the object. Swapping works for value types only. That should be sufficient, if you're following the advice of Item 6.

在一般情況下,你不能修複引用類型交換的問題,隻能保證所有的使用者有目前對象的拷貝。交換隻對值類型适用。如果你遵循了Item 6,那就足夠了。

Last, and most stringent, is the no-throw guarantee. The no-throw guarantee is pretty much what it sounds like: A method satisfies the no-throw guarantee if it is guaranteed to always run to completion and never let an exception leave a method. This just isn't practical for all routines in large programs. However, in a few locations, methods must enforce the no-throw guarantee. Finalizers and Dispose methods must not throw exceptions. In both cases, throwing an exception can cause more problems than any other alternative. In the case of a finalizer, throwing an exception terminates the program without further cleanup.

最後,也是最嚴格的,是無抛出保障。無抛出保障聽起來更像是:如果一個方法總能保障運作完畢并且從不讓異常出現在方法之外的話,該方法就滿足了無抛出異常。在大型程式裡,對所有子程式都這樣是不現實的。然而,你一部分地方,方法必須要強制無抛出保障。 Finalizers和Dispose方法不應該抛出異常。對兩者來說,抛出異常會比做其他改變引起更多的問題。對于finalizer,抛出異常将終止程式,而不做進一步的清理。

In the case of a Dispose method throwing an exception, the system might now have two exceptions running through the system. The .NET environment loses the first exception and throws the new exception. You can't catch the initial exception anywhere in your program; it was eaten by the system. This greatly complicates your error handling. How can you recover from an error you don't see?

對于Dispose方法抛出異常的情況,就有可能出現兩個異常運作在系統裡面的情況。.Net環境放過第一個異常并抛出新的異常。在程式的任何地方你都不能捕獲開始的異常;它被系統給吃掉了。這使得你的錯誤吃力很複雜。你怎麼能從沒有見過的錯誤中恢複呢?

The last location for the no-throw guarantee is in delegate targets. When a delegate target throws an exception, none of the other delegate targets gets called from the same multicast delegate. The only way around this is to ensure that you do not throw any exceptions from a delegate target. Let's state that again: Delegate targets (including event handlers) should not throw exceptions. Doing so means that the code raising the event cannot participate in the strong exception guarantee. But here, I'm going to modify that advice. Item 21 showed that you can invoke delegates so that you can recover from exceptions. Not everyone does, though, so you should avoid throwing exceptions in delegate handlers. Just because you don't throw exceptions in delegates does not mean that others follow that advice; do not rely on the no-throw guarantee for your own delegate invocations. It's that defensive programming: You should do the best you can because other programmers might do the worst they can.

最後一個無抛出保障的地方就是委托目标。當一個委托目标抛出異常時,同一個多點傳播委托裡面的其他委托目标就不能被調用了。關于這個的唯一方式就是保證委托從不抛出異常。讓我們再說一遍:委托目标(包含事件句柄)不應該抛出異常。這樣做意味着:産生事件的代碼不能參與強異常保障。但是,在這裡,我将要修改那條建議。Item 21 告訴你,你可以調用委托,那樣的話就可以從異常裡面恢複。但是,不是所有的人都可以,是以,你應該在委托句柄裡面避免抛出異常。僅僅因為你在委托裡面不抛出異常,并不意味着,其他人也遵守該建議;對你自己的委托調用不要依賴于無抛出保障。那是防禦式程式設計:你應該盡力,因為其他程式員可能做得很差。

Exceptions introduce serious changes to the control flow of an application. In the worst case, anything could have happened or not happened. The only way to know what has and hasn't changed when an exception is thrown is to enforce the strong exception guarantee. Then an operation either completes or does not make any changes. Finalizers, Dispose(), and delegate targets are special cases and should complete without allowing exceptions to escape under any circumstances. As a last word, watch carefully when swapping reference types; it can introduce numerous subtle bugs.

異常對于應用程式的控制流,引入了嚴重的變化。最壞的情況,任何事情都可能發生,什麼事情也可能都不發生。當抛出異常時,要知道什麼改變了,什麼沒有改變,唯一的方式就是:強制強異常保障。那樣的話,一個操作要麼能完成,要麼什麼也不改變。Finalizers、Dispose()和委托目标是特殊情況,應該要執行完,不能讓異常影響外部。最後一句話,當交換引用類型是,要小心;那可能會引入無數的小bug。

繼續閱讀