CLR via C#:异常和状态管理

异常基础:如下所示:
1.在visual studio的监视窗口中,可以添加特殊变量名称$exception来查看当前抛出的异常实例。
2.C#将非Exception派生异常封装在RuntimeWrappedException类的m_wrappedException字段中,然后抛出RuntimeWrappedException异常供使用者捕获处理。
3.调用方法前,可调用RuntimeHelpers类的EnsureSufficientExecutionStack函数来检查栈空间是否够用。当栈空间不够用时,该函数就会抛出一个InsufficientExecutionStackException异常。
4.异常处理会造成额外的性能开销,但是相比异常处理带来的收益是远远大于性能损耗的。

异常结构:由一个try块+零个catch块+一个finally块组成;或者由一个try块+至少一个catch块+可选的一个finally块组成。参考伪代码如下所示:

try
{
	// 需要得体地进行异常恢复和/或资源清理的代码放在这里
}
catch(InvalidOperationException)
{
	// 从InvalidOperationException恢复的代码放这里
}
catch(IOException)
{
	// 从IOException恢复的代码放这里
}
catch
{
	// 从除了上述异常之外的其他所有异常恢复的代码放在这里
	...
	// 如果什么异常都要捕捉,通常要重新抛出异常。
	throw;
}
finally
{
	// 这里的代码对始于try块的任何资源操作进行清理。
	// 不管是否抛出异常,这里的代码总是执行的。
}
// 如果try块没有抛出异常,或者某个catch块捕获到异常,但没有抛出或重新抛出异常,就执行下面代码
...

异常执行流程:当try块的代码中抛出异常时,执行流程如下所示:
1.CLR会从调用栈中按照从下自上的顺序来搜索捕捉类型与抛出的异常相同的catch块。如果没有任何匹配的catch块的话,就会发生未处理异常;否则就执行下面的步骤2。
2.CLR会执行所有内层的finally块中的代码。
3.当所有内层的finally块执行完毕后,匹配异常的那个catch块中的代码才开始执行。catch块的末尾可以提供以下三种操作:
1>.重新抛出相同异常,向调用栈高一层的代码通知该异常的发生。
2>.抛出一个不同的异常,向调用栈高一层的代码提供更丰富的异常信息。
3>.让线程从catch块底部退出。如果存在finally块的话就执行该代码块,然后执行finally块后面的代码;否则就执行最后一个catch块后面的语句。

异常堆栈追踪:Exception类的StackTrace属性用来记录堆栈执行过程。具有以下特性:
1.当构建Exception派生对象时,StackTrace属性值为null。
2.当一个异常抛出时,CLR记录异常抛出位置;一个catch块捕捉到该异常时,CLR记录捕捉位置。当catch块内访问被抛出的异常实例的StackTrace属性时,会创建一个字符串来指出从异常抛出位置到异常捕捉位置的所有函数。
3.CLR只记录最新的异常实例的抛出位置。
4.StackTrace属性返回的字符串不包含调用栈中比接受异常实例的那个catch块高的任何函数。
5.CLR从程序集调试符号(存储在.pdb文件)中获取源代码的文件路径和代码行号。
6.StackTrace属性返回的字符串中如果不存在某些函数信息时,可能由以下原因造成:
1>.调用栈记录的是线程的返回位置,而不是异常的来源位置。
2>.JIT编译器对这些函数进行了内联优化。可以使用以下方案来禁止该优化:
1>>.将函数应用MethodImplAttribute.NoInlining标志。
2>>.使用C#编译器的/debug开关来将程序集应用Debuggabletrribute.DisableOptimizations标志。

自定义异常类:以自定义磁盘满异常为例,流程如下所示:
1.定义异常参数抽象基类。参考代码如下所示:

[Serializable]
public abstract class ExceptionArgs
{
	public virtual String Message { get { return String.Empty; } }
}

2.定义Exception派生类,并处理序列化操作。参考代码如下所示:

[Serializable]
public sealed class Exception<TExceptionArgs> : Exception, ISerializable where TExceptionArgs : ExceptionArgs
{
	private const String ARGS = "Args";
	
	private readonly TExceptionArgs m_args;
	public TExceptionArgs Args
	{
		get { return m_args; }
	}
	
	public Exception(String message = null, Exception innerException = null) : this(null, message, innerException)
	{
	}
	
	public Exception(TExceptionArgs args, String message = null, Exception innerException = null) : base(message, innerException)
	{
		m_args = args;
	}

	// 这个构造函数用于反序列化。由于类是密封的,所以访问权限是私有的;
	// 如果类不是密封的话,访问权限要为保护的
	[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializableFormatter)]
	private Exception(SerializableInfo info, StreamingContext context) : base(info, context)
	{
		m_args = (TExceptionArgs)info.GetValue(ARGS, typeof(TExceptionArgs));
	}

	// 这个函数用于序列化。由于ISerializable接口,所以访问权限是公共的
	[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializableFormatter)]
	public override void GetObjectData(SerializableInfo info, StreamingContext context)
	{
		info.AddValue(ARGS, m_args);
		base.GetObjectData(info, context);
	}

	public override String Message
	{
		get
		{
			String baseMsg = base.Message;
			return (m_args == null) ? baseMsg : baseMsg + " (" + m_args.Message + ")";
		}
	}

	public override Boolean Equals(Object obj)
	{
		Exception<TExceptionArgs> other = obj as Exception<TExceptionArgs>;
		if (other == null)
		{
			return false;
		}
		
		// 比较子类成员和基类成员是否相等
		return Object.Equals(m_args, other.m_args) && base.Equals(obj);
	}

	public override int GetHashCode()
	{
		return base.GetHashCode();
	}
}

3.定义一个磁盘满的异常参数类。参考代码如下所示:

[Serializable]
public sealed class DiskFullExceptionArgs : ExceptionArgs
{
	private readonly String m_diskpath;
	public String DiskPath
	{
		get
		{
			return m_diskpath;
		}
	}
	
	public DiskFullExceptionArgs(String diskpath)
	{
		m_diskpath = diskpath;
	}

	// 重写Message属性来包含自己的Message
	public override String Message
	{
		get
		{
			return (m_diskpath == null) ? base.Message : "DiskPath=" + m_diskpath;
		}
	}
}

4.抛出&捕获磁盘满异常。参考代码如下所示:

try
{
	throw new Exception<DiskFullExceptionArgs>(new DiskFullExceptionArgs(@"C:\"), "the disk is full");
}
catch(Exception<DiskFullExceptionArgs> e)
{
	Console.WriteLine(e.Message);
}

约束执行区域:CER是创建可靠托管代码机制的一部分。使用流程如下:
1.在try语句块的前面调用RuntimeHelpers.PrepareConstrainedRegions函数,此时JIT就会遍历整个调用图来提前编译与try语句块关联的catch和finally语句块中的具有ReliabilityContractAttribute定制特性的代码。如果提前编译的代码造成异常,那么这个异常会在线程进入try块之前抛出。
2.ReliabilityContractAttribute定制特性可以应用于接口,构造函数,类,程序集或者函数等。该定制特性内部使用Cer和Consistency枚举类型来提供可靠性协定。其中Cer的定义如下:

enum Cer
{
	None, // 不进行任何CER保证
	MayFail, // 可能会失败
	Success, // 一定会成功
}

Consistency的定义如下:

enum Consistency
{
	MayCorruptProcess, // 可能损坏进程状态
	MayCorruptAppDomain, // 可能损坏AppDomain状态
	MayCorruptInstance, // 可能损坏实例状态
	WillNotCorruptState, // 不损坏任何状态
}

3.编译器和CLR不会验证提前编译的代码是否真的符合通过ReliabilityContractAttribute来做出的保证。所以如果犯了错误,状态仍有可能损坏。
4.即使提前编译的代码都准备好了,在调用时仍有可能造成StackOverflowException。在CLR没有寄宿的前提下,StackOverflowException会造成CLR在内部调用Environment.FailFast来立即终止进程。在已经寄宿的前提下,RuntimeHelpers.PrepareConstrainedRegions函数会检查是否剩下约48KB的栈空间。当栈空间不足时就在进入try语句块前抛出StackOverflowException。
5.可以使用RuntimeHelpers.ExecuteCodeWithGuaranteedCleanUp函数来在资源保证得到清理的前提下调用该函数。
6.可以使用RuntimeHelpers.PrepareMethod或者RuntimeHelpers.PrepareDelegate以及RuntimeHelpers.PrepareContractedDelegate函数来创建可靠的函数。

代码协定:提供了直接在代码中声明代码设计决策的一种方式。具有以下特性:
1.类型为Contract,具有以下特性:
1>.前置条件:一般用于对实参进行验证。
2>.后置条件:函数因为一次普通的返回或者抛出异常而终止时,对状态进行验证。
3>.对象不变性:在对象的整个生命周期内,确保对象的字段的良好状态。
4>.前置条件,后置条件和对象不变性测试中引用的任何成员都一定不能改变对象的状态。
5>.前置条件测试中引用的成员的可访问性都至少要和定义前置条件的函数一样;后置条件和对象不变性测试中引用的成员具有任何可访问性,只要代码能编译就行。
6>.派生类型不能重写并更改基类型中定义的虚成员的前置条件。
7>.为了兼容之前的代码协定,新版本的代码中协定不能更严格。
2.工具为Microsoft Code Contracts,具有以下特性:
1>.属性页勾选Perform Runtime Contract Checking之后,Visual Studio会在生成项目时自动调用位于C:/Program Files(x86)/Microsoft/Contracts/Bin目录下的CCRewrite.exe。
2>.CCRewrite.exe会使任何后置条件的协定都在函数的末尾执行。
3>.CCRewrite.exe会在类型中查找标记了[ContractInvariantMethod]特性的任何函数。当查找到该函数时,CCRewrite.exe就会在每一个公共实例函数的末尾插入调用该函数的IL代码,从而确保实例函数在调用时是否有违反协定。
4>.代码运行时违反协定会触发Contract的ContractFailed事件。具有以下处理情况:
1>>.注册了ContractFailed事件的处理函数时会收到一个ContractFailedEventArgs对象。可以调用该对象的SetHandled函数来忽略违反协定的情况;可以调用该对象的SetUnwind函数来强制抛出ContractException;可以使用默认的方式进行处理。
2>>.没有注册了ContractFailed事件的处理函数时会使用默认的方式进行处理。
3>>.默认的方式处理流程如下:
1>>>.如果CLR已经寄宿,会向宿主通知协定失败。
2>>>.如果CLR正在非交互式窗口站上运行应用程序,会调用Environment.FailFast来立即终止进程。
3>>>.如果编译时在属性页中勾选了Assert On Contract Failure,就会出现一个断言对话框,此时允许将一个调试器连接到你的应用程序;否则就会抛出ContractException。
5>.属性页勾选Perform Static Contract Checking之后,Visual Studio会在生成项目时自动调用位于C:/Program Files(x86)/Microsoft/Contracts/Bin目录下的CCCheck.exe。
6>.CCCheck.exe会分析C#编译器生成的IL,静态验证函数中是否有代码违反协定。
7>.CCCheck.exe会尝试证明Assert的任何条件都为true以及假设传给Assume的任何条件都已经为true。一般建议先用Assert,然后在CCCheck.exe不能静态证明表达式为true的前提下将Assert更改为Assume。
8>.CCRefGen.exe工具用来创建独立的,包含协定的一个引用程序集。该程序集名字为AssemblyName.Contract.dll且只包含对协定进行描述的元数据和IL。
9>.CCDocGen.exe用来生成具有MSDN风格的,含有协定信息的文档。

用可靠性换取开发效率:可以使用以下方案来缓解抛出异常对状态的破坏:
1.执行catch或finally块中的代码时,CLR不允许线程终止。
2.利用Contract类向函数应用代码协定。通过代码协定,在用实参/变量对状态进行修改之前,可以先对这些实参/变量进行验证。
3.利用CER来消除CLR的某些不确定性。
4.利用事务机制来确保状态要么都修改,要么都不修改。
5.如果觉得状态已经损坏到无法修复的程度,就应该使用AppDomain的Unload函数来卸载整个AppDomain,以防止它造成更多的伤害。
6.如果觉得状态过于糟糕,以至于整个进程都应该终止,那么应该调用Environment的静态FailFast函数。该函数具有以下特性:
1>.在终止进程时,不会调用任何活动的try/finally块或者Finalize函数。
2>.为CriticalFinalizerObject实例提供进行回收的机会。
3>.将消息字符串和可选的异常实例写入Windows Application事件日志,生成Windows错误报告,创建应用程序的内存转储,最后终止当前进程。

设计规范和最佳实践:如下所示:
1.善用finally块。当使用lock,using,foreach语句或者重写类的析构函数时,编译器自动生成try/finally块。其中try块中放入开发者写的代码;finally块中放入清理资源代码。参考伪代码如下所示:

using(FileStream fs = new FileStream(@"C:\Data.bin", FileMode.Open))
{
	Console.WriteLine(100 / fs.ReadByte());
}
// 等价于
FileStream fs = new FileStream(@"C:\Data.bin", FileMode.Open)
try
{
	Console.WriteLine(100 / fs.ReadByte());
}
finally
{
	fs.close()
}

2.不要什么异常都捕捉。如果非要捕捉所有异常的话,最好使用throw重新抛出来交给上层去处理。
3.得体的从异常中恢复。
4.发生不可恢复的异常时回滚部分完成的操作。
5.隐藏实现细节来维系协定。也就是在捕获的异常里面抛出调用者希望抛出的异常。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值