面向对象编程极大的提升了开发人员的效率。开发效率的提升有很大一部分来源于可组合性,它使代码很容易编写、阅读、维护。例如下面的代码:
Boolean f = "Jeff".Substring(1, 1).ToUpper().EndsWith("E");
但上面代码有一个重要的前提:没有错误发生。而错误总是可能发生的。所以,我们需要一种方式处理那些错误。这正是异常处理构造和机制的目的,也解释了我们为什么不让自己的方法像Win 32和Com函数那样返回true/fasle来指出成功/失败。
除了代码的可组合性,开发效率的提升还来自编译器提供的各种好用的功能。例如:编译器能隐士的做下面这些事情。
10. 定义/构造/初始化匿名类型及其实例。
11. 重新写代码来支持LINQ查询表达式和表达式数。
另外 ,CLR还提供了大量的辅助,进一步方便我们编程。例如,CLR会隐式的做下面这些事情。
另外,理所当然,.net FrameWork配套提供了包罗万象的类库,其中有数以万计的类型,每个类型都封装了常用的、可重用的功能。可利用这些类型构造Web窗体应用程序、Web服务和富GUI应用程序,可以处理安全性、图像和语音识别等。所有这些代码都可能抛出指明某个地方出错的异常。另外,未来的版本可能引入从现有异常类型派生的新异常类型,而你的catch块现在能捕捉以前从未存在过的异常类型。
所有这些东西-面向对象编程、编译器功能、CLR功能以及庞大的类库-使.net framework成为一个颇具吸引力的软件平台。但我的观点是,所有这些东西都会在你的代码中引入你没有什么控制权的“错误点“.如果所有东西都正确无误的运行,那么一切都好;可以方便的编写代码,写出来的代码也很容易维护和阅读。但是,一旦某样东西出错,就几乎不可能完全理解哪里出错和为什么出错。下面这个例子可以证明我的观点:
private static Object OneStatement(Stream stream, Char charToFind)
{
return (charToFind + ":" + stream.GetType() + String.Empty + (stream.Position + 512M)).Where(c => c == charToFind).ToArray();
}
这个不太自然的方法只包含C#语句。但这个语句做了大量工作。下面是C#编译器为这个方法生成的IL代码(一些行加粗倾斜显示,由于一些隐士的操作,他们成为潜在错误点):
.method private hidebysig static object OneStatement(class [mscorlib]System.IO.Stream 'stream',
char charToFind) cil managed
{
// 代码大小 127 (0x7f)
.maxstack 5
.locals init ([0] class ConsoleApplication1.Program/'<>c__DisplayClass1' 'CS$<>8__locals2',
[1] object CS$1$0000,
[2] object[] CS$0$0001)
IL_0000: newobj instance void ConsoleApplication1.Program/'<>c__DisplayClass1'::.ctor()
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: ldarg.1
IL_0008: stfld char ConsoleApplication1.Program/'<>c__DisplayClass1'::charToFind
IL_000d: nop
IL_000e: ldc.i4.5
IL_000f: newarr [mscorlib]System.Object
IL_0014: stloc.2
IL_0015: ldloc.2
IL_0016: ldc.i4.0
IL_0017: ldloc.0
IL_0018: ldfld char ConsoleApplication1.Program/'<>c__DisplayClass1'::charToFind
IL_001d: box [mscorlib]System.Char
IL_0022: stelem.ref
IL_0023: ldloc.2
IL_0024: ldc.i4.1
IL_0025: ldstr ":"
IL_002a: stelem.ref
IL_002b: ldloc.2
IL_002c: ldc.i4.2
IL_002d: ldarg.0
IL_002e: callvirt instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
IL_0033: stelem.ref
IL_0034: ldloc.2
IL_0035: ldc.i4.3
IL_0036: ldsfld string [mscorlib]System.String::Empty
IL_003b: stelem.ref
IL_003c: ldloc.2
IL_003d: ldc.i4.4
IL_003e: ldarg.0
IL_003f: callvirt instance int64 [mscorlib]System.IO.Stream::get_Position()
IL_0044: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Implicit(int64)
IL_0049: ldc.i4 0x200
IL_004e: newobj instance void [mscorlib]System.Decimal::.ctor(int32)
IL_0053: call valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Addition(valuetype [mscorlib]System.Decimal,
valuetype [mscorlib]System.Decimal)
IL_0058: box [mscorlib]System.Decimal
IL_005d: stelem.ref
IL_005e: ldloc.2
IL_005f: call string [mscorlib]System.String::Concat(object[])
IL_0064: ldloc.0
IL_0065: ldftn instance bool ConsoleApplication1.Program/'<>c__DisplayClass1'::'<OneStatement>b__0'(char)
IL_006b: newobj instance void class [mscorlib]System.Func`2<char,bool>::.ctor(object,
native int)
IL_0070: call class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0> [System.Core]System.Linq.Enumerable::Where<char>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>,
class [mscorlib]System.Func`2<!!0,bool>)
IL_0075: call !!0[] [System.Core]System.Linq.Enumerable::ToArray<char>(class [mscorlib]System.Collections.Generic.IEnumerable`1<!!0>)
IL_007a: stloc.1
IL_007b: br.s IL_007d
IL_007d: ldloc.1
IL_007e: ret
} // end of method Program::OneStatement
如你所见,构造<>c__DisplayClass1类(编译器生成的一个类型)、Object[]数据和Func委托,以及对char和Decimal进行装箱时,可能抛出OutOfMemoryException、调用Concat、Where、和ToArray时,也会在内部分配内存。构造Decimal实例时,可能造成他的类型构造器被调用,并抛出一个TypeInitializationException。还存在decimal的op_Implicit操作符和op_Addition操作符方法的隐士调用,这些方法可能抛出一个OverflowException.
查询stream的Position属性比较有趣。首先,它是一个虚属性,所以我的OneStatement方法无法知道实际执行的代码,它可能抛出任何异常。其次,Stream从MarshalByRefObject派生,所以Stream可能引用一个代理对象,后者又引用一个AppDomain中的对象。而另一个AppDomain已经卸载,造成抛出一个AppDomainUnloadedException。
当然被调用的所有方法都是我个人无法控制的方法,因为他们都是由微软生成的。另外,微软将来可能完全更改这些方法的实现,使他们抛出当我写OneStatement方法时不可能知道的一些新的异常信息。所以,我怎可能写这个OneStatement方法来获得完全的”健壮性”来防范可能发生的错误呢?顺便说一句,反过来也会成为问题:一个catch块可能捕捉到从指定的异常类型派生的一个异常类型,造成要为一种不同的错误恢复代码。
对所有的错误有了一个基本认识之后,就可以理解为何不去追求完美健壮和可靠的代码,因为不且实际,不去追求完美和健壮性代码另一个原因是错误不经常发生。由于错误及其罕见,所以开发人员不去追求完全可靠的代码,牺牲一些可靠性来换取程序员的开发效率的提升。
关于异常的意见好事在于,未处理的异常会造成应用程序终止。之所以说这是一件好事,是因为在测试期间快速发现错误。利用由未处理的异常提供的信息,通常足以完成对代码的修正。当然,许多公司不希望他们的应用程序在测试和部署之后继续发生意外终止情况,所以他们会插入代码来捕捉Exception,也就是所有异常类的基类。然而,如果捕捉Exception并允许应用程序运行,一个很大的问题是状态可能遭受破坏。
本章早些时候展示了一个Account类,它定义了一个Transfer方法,用于将钱从一个账户转移到另一个。当这个Transfer方法调用时,如果成功将钱从from账户扣除,但是将钱添加到to账户之前抛出一个异常,那么会发生什么?如果调用代码,捕捉Exception并继续运行,应用程序的状态就会被破坏:from和to账户的钱都可能被变少。由于涉及到金钱,所以这种对状态的破坏不能视为一个简单的bug,而是应该被视为一个安全性的bug。假如应用程序继续运行,会尝试对大量账户执行更多的转账操作,造成对状态破坏在应用程序中蔓延。
一些人可能会说,Transfer方法本省应该捕捉Exception,并将钱还给from账户,如果Transfer方法非常简单,这个方案确实可行。但是,如果Transfer还要生成一条关于取钱的审计记录,或者其他线程同时操作这个账户,那么撤销操作本身是可能失败的,造成抛出另一个异常。现在,状态破坏变的更加糟糕了,并非更好。
为了缓解对状态的破换,可以做下面几件事情:
1.执行catch和finally块中的代码时,CLR不允许线程终止。所以可以想下面这样写Transfer方法变得更健壮:
public static void Transfer(Account from, Account to, Decimal amount)
{
Try{}
Finally{from-=amount;to+=amount;}
}
但是,绝对不建议将所有代码都放到finally块中!这个技术只适用于修改及其敏感的状态。
2.可以用Contract类向方法应用代码契约。利用代码契约,在实参和其他变量对状态修改之前,可以先对这些实参和变量进行验证。如果实参和变量遵守契约,状态破换的可能性将大幅降低。如果不遵守契约,那么异常可能在任何状态被修改之前抛出。后面讲讨论代码契约。
3.可以使用执行约束区域(Constrained Execution Region,CER),它提供了消除一些CLR不确定性的一种方式。例如,进入一个try块之前,可以让CLR加载与这try块关联的任何catch和finally块所需的程序集。除此之外,CLR会编译catch块和finally块中的所有代码,包含从这些块中调用的所有方法。这样一来,在尝试执行catch块中的错误恢复代码或者finally块中的清理代码时,就可以消除众多潜在的异常。它还减少了发生OutOfMenoryException和其他一些异常的机率。
4.取决于状态存在于何处,可以利用事物要么都修改,要么都不修改。例如,如果数据在数据库中,事物可以很好的工作。Window现在还支持事物式的注册表和文件操作(仅限于NTFS),所以也许能利用它。但是,.NET FRAMEWORK目前没有直接公开这个功能。请参见TransactionScope类了解细节。
在你的代码中,如果确定状态已经损坏到无法修复的程度,就应该销毁所有损坏的状态,防止造成更多的伤害。然而,重新启动应用程序,将状态初始化到一个良好的状态,并寄望与状态不在损坏。由于一个托管的状态不能泄露到一个AppDomain的外部,所以为了销毁一个AppDomain中所有损坏的状态。可以调用AppDomain的Unload方法来卸载正个AppDomain.
如果觉得状态过于槽糕,以至于整个进程都应该终止,那么应该调用Environment的静态FailFast方法。
这个方法在终止进程时,不会运行任何活动的try/finally块或者Finalize方法。之所以可以这么做,是因为在状态损坏的前提下执行更多的代码,很容易使局面变的更坏。不过FailFast允许从 CriticalFinalizerObject派生的任何对象,从而提供一个进行清理的机会,之所以允许他们,是因为他们一般只是关闭本地资源;即使CLR或者你的应用程序状态发生损坏,Window状态也有可能是好的,FailFast方法将消息字符串和可选的异常(catch中捕捉的异常)发送给Window Application事件日志,生成一个Window错误报告,创建应用程序的一个内存dump,然后终止当前进程。