Effective C# 优先选择强异常安全保证

当你抛出异常时,你就在应用程序中引入了一个中断事件。而且危机到程序的控制流程。使得期望的行为不能发生。更糟糕的是,你还要把清理工作留给最终写代码捕获了异常的程序员。而当一个异常发生时,如果你可以从你所管理的程序状态中直接捕获,那么你还可以采取一些有效的方法。谢天谢地,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接口的资源对象.解释了如何在对面异常时避免资源泄漏。

        强异常保证状态是指,如果一个操作因为某个异常中断,程序维持原状态不改变。不管操作是否完成,都不修改程序的状态,这里没有折衷。强异常保证的好处是,你可以在捕获异常后更简单的继续执行程序,当然也是在你遵守了强异常保证情况下。任何时候你捕获了一个异常,不管操作意图是否已经发生,它都不应该开始了,而且也不应该做任何修改。这个状态就像是你还没有开始这个操作行为一样。

        很多我所推荐的方法,可以更简单的帮助你来确保进行强异常保证。你程序使用的数据元素应该存为一个恒定的类型。如果你组并这两个原则,对程序状态进行的任何修改都可以在任何可能引发异常的操作完成后简单的发生。常规的原则是让任何数据的修改都遵守下面的原则:

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对象的引用。所有的类的使用客户,在你调用了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内容已经更新。

        在一般情况下,你不能修正像这样的引用类型交换,然后还想确保用户拥有当前的对象拷贝。交换工作只对值类型有效,这应该是足够了。

 

最后,也是最严格的,就是无抛出保证。无抛出保证听起来很优美,就像是:一个方法是无抛出保证,如果它保证总是完成任务,而且不会在方法里发生任何异常。在大型程序中,对于所有的常规问题并不是实用的。然而,如果在一个小的范围上,方法必须强制无抛出保证。析构和处理方法就必须保证无异常抛出。在这两种情况下,抛出任何异常会引发更多的问题,还不如做其它的选择。在析构时,抛出异常中止程序就不能进一步的做清理工作了。

        如果在处理方法中抛出异常,系统现在可能有两个异常在运行系统中。.Net环境丢失前面的一个异常然后抛出一个新的异常。你不能在程序的任何地方捕获初始的异常,它被系统吞掉了。这样,你又如何能从你看不见的错误中恢复呢?

        最后一个要做无抛出保证的地方是在委托对象上。当一个委托目标抛出异常时,在这个多播委托上的其它目标就不能被调用了。

        对于这个问题的唯一方法就是确保你在委托目标上不抛出任何异常。否则的话,触发事件的代码就无法获得“强异常保证”。对此建议还要补充一点,曾向大家展示了一种技巧,利用它我们可以在调用委托链时不受某个方法抛出异常影响而中断。但并不是每个人都这样做,所以我们还是应该避免在委托处理器中抛出异常。我们不抛出异常,并不意味着其它人也不抛出异常。因此,在我们做委托调用时,不能指望它是无抛出的保证。这也是防御性编程的精神:我们应该尽可能做好我们的工作,因为其他程序可能编写糟糕的代码。

        异常列在应用程序的控制流程上引发了一系列的改变,在最糟糕的情况下,任何事情都有可能发生,或者任何事情也有可能不发生。在异常发生时,唯一可以知道哪些事情发生了,哪些事情没有发生的方法就是强制强异常保证。在强异常安全保证下,一个操作或者是成功完成,或者对程序状态不做任何修改。以下三种构造比较特殊:终结器、Dispose()以及委托对象所绑定的目标方法,在任何情况下,我们都应该确保它们不会抛出异常。最后一句话:小心对引用类型的交换,它可能会引发大量潜在的BUG。


 

posted @ 2008-11-14 09:03  瞪着你的小狗  阅读(585)  评论(0编辑  收藏  举报