Effective C# 原则45:选择强异常来保护程序
Item 45: Prefer the Strong Exception Guarantee
当你抛出异常时,你就在应用程序中引入了一个中断事件。而且危机到程序的控制流程。使得期望的行为不能发生。更糟糕的是,你还要把清理工作留给最终写代码捕获了异常的程序员。而当一个异常发生时,如果你可以从你所管理的程序状态中直接捕获,那么你还可以采取一些有效的方法。谢天谢地,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。
===================================
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#.
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.
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.
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:
1. Make defensive copies of data that will be modified.
2. Perform any modifications to these defensive copies of the data. This includes any operations that might throw an exception.
3. 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:
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;
}
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:
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;
}
}
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.
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 typesit works only for value types. Because it is a common operation, there is a specific fix for DataSets. Use the Merge method:
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 );
}
}
Merging the changes into the current DataSet lets all clients keep a valid reference, and the internal contents of the DataSet are updated.
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.
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.
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?
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.
Exceptions introduce serious changes to the control flow of an application. In the worst case, anything could have happenedor 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.