设计规范和最佳实践
善用finally块
finally块
非常厉害, 无论线程抛出什么类型的异常,finally块
中的代码都会执行. 经常利用finally块
显式释放对象以避免资源泄露.
确保清理代码的执行时很重要的, 以至于许多语言提供了一些构造来简化这种代码. 例如:
lock
- 锁在
finally块
中释放
- 锁在
using
- 在
finally块
中调用对象的Dispose
方法
- 在
foreach
- 在
finally块
中调用IEnumerator对象的Dispose方法.
- 在
- 定义析构器方法(
Finalize方法
)时- 在
finally块
中调用基类的Finalize方法
- 在
C#编译器会自动生成try/finally块. 重写类的析构器Finalize方法
时, 也会生成try/finally块.使用这些构造时,编译器将你写的代码放到try块
内部, 并将清理代码放到finally块
中.
不要什么都捕捉
绝对不要写大小通吃的类型,悄悄地吞噬异常, 而是应该允许异常在调用栈中向上移动,让应用程序代码针对性地处理它.
吞噬 swallow, 也有作者喜欢说bury, 有人也翻译为隐藏, 简单的说就是自己搞定异常,然后装作异常没有发生.
确实可以在catch块中捕捉System.Exception并执行一些代码, 只要在这个catch块的末尾重新抛出异常. 千万不要捕捉System.Exception异常并悄悄吞噬它而不重新抛出, 否则应用程序不知道已经出错,还是会继续运行.
可以在一个线程中捕捉异常, 在另一个线程中重新抛出异常. 为此提供支持的是异步编程模型.
例如,假定在一个线程池线程执行的代码抛出了异常,CLR捕捉并吞噬这个异常, 并允许线程池返回线程池,稍后会有某个线程调用EndXXX方法来判断异步操作的结果, EndXXX方法将抛出与负责实际工作的那个线程池抛出的一样的异常. 所以, 异常虽然被第一个方法吞噬,但又被调用EndXXX的线程重新抛出,这样,该异常在应用程序面前就不是隐藏的了.
得体地从异常中恢复
try
{
// 计算电子表格单元格中的值
}
catch (DivideByZeroException)
{
"不能显示值, 除数为0";
}
catch (OverflowException)
{
"不能显示值, 溢出"
}
捕捉具体异常时, 应充分掌握在什么时候会抛出异常, 并知道从捕捉的异常类型派生出了哪些类型, 不要捕捉System.Exception(除非你会重新抛出).
发生不可恢复的异常时回滚部分完成的操作—-维持状态
public void SerializeObjectGraph(FileStream fs, IFormatter formatter, Object rootObj)
{
// 保存文件的当前位置
Int64 beforeSerialization = fs.Position;
try
{
// 尝试将对象图序列化到文件中
formatter.Serialize(fs, rootObj);
}
catch // 捕捉所有异常
{
// 任何事情出错,就将文件恢复到一个有效状态
fs.Position = beforeSerialization;
// 截断文件
fs.SetLength(fs.Position);
// 注意: 上述代码没有放到finally块中, 因为只有在序列化失败时才对流进行重置
// 重新抛出相同的异常,让调用者知道发生了什么
thorw;
}
}
为了正确回滚已部分完成的操作, 代码应捕捉所有异常. 是的,这里要捕捉所有异常, 因为你不关心发生了什么错误, 只关心如何将数据结构恢复为一致状态. 捕捉并处理好异常后.不要把它吞噬(假装没发生), 想法,要让调用者知道发生了什么异常, 只需要单独使用C#的关键字throw
, 不在关键字throw
后指定任何东西.
隐藏实现细节来维系协定
有时需要捕捉一个异常并重新抛出不同的异常. 这样做唯一的原因是维系方法的协定(contract). 另外,抛出的新异常类型应该是一个具体异常(不是其他异常的基类).
例子(伪代码):
文件由于任何原因未找到或者不能读取, 调用者将看到一个FileNotFoundException或者IOException异常, 因为这两个异常都不是调用者预期的, 因为文件存在与否以及能否读取不是方法的隐式协定的一部分, 调用者根本猜不到(猜不到PhoneBook类的方法要从文件中读取数据). 所以GetPhoneNumber方法
会捕捉这两种异常,抛出一个新的NameNotFoundException
异常.
NameNotFoundException
异常为调用者提供了理解其中原因的一个抽象视图,将内部异常设为FileNotFoundException或者IOException异常 是非常重要的一环,这样才能保证不丢失造成异常的真正原因.
这个技术会使调试变得困难,务必慎用. 有时开发人员之所以捕捉一个异常并抛出一个新异常,目的是在异常中添加额外的数据或上下文. 如果只是这个目的,那么只需捕捉希望的异常类型,在异常对象的Data属性(一个键值对)中添加数据, 然后重新抛出相同的异常对象:
catch(IOException e) { e.Data.Add("Filename",filename); throw; }
将文件名添加到IOException对象中,这样重新抛出同一个异常对象,只是它现在包含额外的数据.
未处理的异常
异常抛出时, CLR在调用栈中向上查找与抛出的异常对象的类型匹配的catch块. 没有任何catch块匹配抛出的异常类型. 就会发生一个未处理的异常. CLR检测到任何线程中的未处理异常都会终止进程.
当应用程序的开发人员才需关心未处理的异常. 应用程序发生未处理的异常时, windows会向事件日志写一条记录. 查看该记录: 事件查看器-> windows日志->应用程序节点
系统托盘中的小旗->打开操作中心->展开维护->单击查看可靠性历史记录
会在底部的窗格看到应用程序由于未处理的异常而终止.
对异常进行调试
针对VS. 具体看书.
异常处理的性能问题
个别时候会遇到频繁调用但频频失败的方法, 这时抛出异常所造成的性能损失可能令人无法接受. 例如: 在调用Int32
的Parse
方法时, 最终用户经常输入无法解析的数据, 由于频繁调用Parse
方法, 抛出和捕捉异常对总体性能造成了很大影响. 为了解决这个问题,Microsoft为Int32类型添加了新的TryParse方法.
public static Boolean TryParse(String s, out Int32 result);
public static Boolean TryParse(String s, NumberStyle style, IFrmatProvider provider, out Int32 result);
这些方法都返回一个Boolean
,指明传给方法的String
是否包含了能解析成Int32
的字符. 它们同时返回一个名为result
的输出参数.
- 如果方法返回
true
,result
将包含字符串解析成32位整数的结果 - 返回
false
,result
将包含0, 这时自然不应再执行任何代码去检查result.- 如果TryXX方法的Boolean返回值为false,那么代表的只是一种错误. 方法仍要为其他错误抛出异常.
- 例如: 为
style参数
传递的实参无效,Int32
的TryParse
方法会抛出一个ArgumentException
异常,另外调用TryParse
方法时仍有可能抛出一个OutMemoryException
异常
定义类型成员时,应确保在一般使用情形中不会失败. 只有用户以后因为抛出异常而堆性能不满意时, 才应考虑添加以下TryXxx
方法.
约束执行区域(CER)
许多应用程序都不需要健壮到能从任何错误恢复的地步.
CLR中, 我们有包含了状态的AppDomain. 卸载时,它的所有状态都会卸载. 所以,如果AppDomain中的一个线程遭遇未处理的异常,可以在不终止整个进程的情况下卸载AppDDomain(销毁它的所有状态).
线程的整个生命周期都在一个AppDomain的内部. 这个说法是成立的.
根据定义, CER是必须对错误有所适应的代码块. 一般用CER处理多个AppDomain或进程共享的状态, 称这些异常为异步异常. CER就非常有用.
例如, 调用方法时,CLR必须加载程序集,在AppDomain
的Loader堆
中创建类型对象
, 调用类型的静态构造器
,并将IL
代码JIT编译成本机代码. 这些操作都可能失败,CLR通过抛出异常来报告失败.
如果任何这些操作再一个catch或finally块中失败, 你的错误恢复或资源清理代码就不会完整的执行.
private static void Demo1()
{
Console.WriteLine("In Demo1");
try
{
Console.WriteLine("In try");
}
finally
{
// 隐式调用Type1的静态构造器
Type1.M();
}
}
private sealed class Type1
{
static Type1()
{
// 如果这里抛出异常,M就得不到调用
Console.WriteLine("Type1's static ctor called");
}
public static void M()
{}
}
上述代码存在一个问题,如果在静态构造器中发生异常,M就得不到调用, M就得不到调用.
想要达到的目的是,除非 保证(大致保证) 关联的catch
和finally
块中的代码得到执行, 否则上述try块
的代码根本不要开始执行.为了这个目的,修改成下面这样:
保证和大致保证分别对应后文所说的
WillNotCorrupState
和MayCorrupInstance
两个枚举成员
private static void Demo2()
{
Console.WriteLine("In Demo2");
// JIT编译器如果发现一个`try块`之前调用了这个方法,就会提前编译与`try块`关联的`catch`和`finally块`中的代码.
// 强迫finally块中的代码提前准备好
RuntimeHelpers.PrepareConstrainedRegions(); //在 System.Runtime.CompilerServices命名空间
try
{
Console.WriteLine("In try");
}
finally
{
// 隐式调用Type2的静态构造器
Type2.M();
}
}
public class Type2
{
static Type2()
{
Console.WriteLine("Type2's static ctor called");
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
public static void M()
{}
}
PrepareConstrainedRegions
是一个很特别的方法, JIT编译器如果发现一个try块
之前调用了这个方法,就会提前编译与try块
关联的catch
和finally块
中的代码. JIT编译器会加载任何程序集, 创建任何类型对象,调用任何静态构造器,并对任何方法进行编译. 如果其中任何操作造成了异常, 这个异常会在线程进入try块
之前 发生.
JIT编译器提前准备方法时, 还会遍历整个调用图, 提前准备被调用的方法, 前提是这些方法应用了[ReliabilityContract]
特性. 向这个特性实例的构造器传递的是WillNotCorrupState
或者MayCorrupInstance
枚举成员.
这是由于加入方法会损坏AppDomain或进程的状态,CLR便无法对一致性做出任何保证. 在通过一个PrepareConstrainedRegions
调用来保护的一个catch或finally块
中, 请确保只条用根据刚才的描述设置了[ReliabilityContract]
的方法.
此特性允许开发者向方法的潜在调用者申明方法的可靠性协定.
如何使用这个特性
RuntimeHelpers的另一个方法
代码协定
协定默认只作为文档使用, 为了发掘协定的附加价值,必须下载额外的工具和一个VS属性窗格.
以下选看:
代码协定 提供了直接在代码中声明代码设计决策的一种方式. 这些协定采取以下的形式:
- 前条件
- 一般用于对实参进行验证
- 后条件
- 方法因为一次普通的返回或者抛出异常而终止时,对状态进行验证
- 对象不变性
- 在对象的整个生命期内,确保对象的字段的良好状态
可将上述想象成方法签名的一部分.
代码协定的核心是静态类 System.Diagnostics.Contracts.Contract
// 源码
public static class Contract
{
// 前条件方法: [Conditional("CONTRACTS_FULL")]
public static void Requires(Boolean condition);
public static void EndContractBlock();
// 前条件: Always
public static void Requires<TException>(Boolean condition)
where TException : Exception;
// 后条件方法: [Conditional("CONTRACTS_FULL")]
public static void Ensures(Boolean condition);
public static void EnsureOnThrow<TException>(Boolean condition)
where TException : Exception;
// 特殊后条件方法 Always
public static T Result<T>();
public static T OldValue<T>(T value);
public static T ValueAtReturn<T>(out T value);
// 对象不变性方法 [Conditional("CONTRACTS_FULL")]
public static void Invariant(Boolean condition);
.....//限定符方法,辅助方法,基础结构事件 请查看源码
}
[Conditional("CONTRACTS_FULL")]
或[Conditional("DEBUG")]
除非定义了恰当的符号,否则编译器会忽略调用这些方法的任何代码. 协定默认只作为文档使用, 因为生成项目没有定义CONTRACTS_FULL
符号
标记Always
的任何方法意味着编译器总是生成调用方法的代码.
Requires
,Requires<TException>
,Ensures
,EnsureOnThrow<TException>
,Invariant
,Assert
,Assume
方法有一个额外的重载版本这里没有列出, 它获取一个String实参
,用于显式指定违反协定时显示的字符串消息.
internal static class CodeContracts
{
public static void Go()
{
var shoppingCart = new ShoppingCart();
shoppingCart.AddItem(new Item());
}
public sealed class Item
{
/* ... */
}
public sealed class ShoppingCart
{
private List<Item> m_cart = new List<Item>();
private Decimal m_totalCost = 0;
public ShoppingCart()
{
}
public void AddItem(Item item)
{
AddItemHelper(m_cart, item, ref m_totalCost);
}
private static void AddItemHelper(List<Item> m_cart, Item newItem, ref Decimal totalCost)
{
// 前条件
// 1. 前条件指出newItem不能为null
// 2. 而且要添加的商铺不在购物车中
Contract.Requires(newItem != null);
Contract.Requires(Contract.ForAll(m_cart, s => s != newItem));
// 后条件
// 1. 新商品必须在购物车中
// 2. 而且总价格至少要与将商铺添加到购物车之前一样多
// 3. 因为某个原因抛异常,那么totalCost不发生变化
Contract.Ensures(Contract.Exists(m_cart, s => s == newItem));
Contract.Ensures(totalCost >= Contract.OldValue(totalCost));
Contract.EnsuresOnThrow<IOException>(totalCost == Contract.OldValue(totalCost));
// 做一些事情,可能抛出IOException
m_cart.Add(newItem);
totalCost += 1.00M;
//throw new IOException(); // 证明违反协定
}
// 对象不变性
[ContractInvariantMethod]
private void ObjectInvariant()
{
// 确保对象的m_totalCost永远不包含负值
Contract.Invariant(m_totalCost >= 0);
}
}
后续不知道在讲啥玩意… 工作原理..