异常过滤器的好坏(CLR)
为什么有些语言支持它们而另一些不支持呢?把它们加到我的新语言里是个好主意吗?我应该什么时候使用过滤器和catch/rethrow?就像很多事情一样,异常过滤器有好的一面也有坏的一面…
什么是异常过滤器?
CLR提供了许多高级语言可以构建的异常处理原语。有些是相当明显的,并且很容易映射到我们大多数人都知道和喜欢的语言结构:例如try/catch和try/finally。我敢猜测每个人都知道他们在做什么,但为了以防万一,让我们考虑一下C#:
try{try{
Console.Write(“1”);if (P) throw newArgumentException();
}finally{
Console.Write(“2”);
}
}catch(ArgumentException e)
{
Console.Write(“3”);
}
Console.Write(“4”);
Visual Studio调试器指南---Watch 1-4窗口
当处于调试模式中断下,我们可以通过Watch窗口快速查看我们想观察的变量、表达式的值。
打开
- 通过菜单栏方式
- 通过代码窗口右键
选中表达式/变量 - 快捷键
先按Ctrl+Alt+W
状态栏显示
在按下数字键1,2,3,4
显示对应的窗口
窗口使用
具体操作跟其他变量窗口操作一样。只是右键菜单多几个选项
- Delete Watch
删除选择的变量或表达式的观察 - Clear All
删除所有的变量和表达式
说明
Watch窗口可以在用户单步调试代码时监视变量的值。尽管一共有4个窗口,但大度数情况使用一个窗口就够了。4个独特的窗口意味着可以把不同类型的变量分别显示在不同窗口。如果开发人员处理的是涉及多个类的复杂问题,这种显示方法就会非常有用。
Watch窗口可以随时任意添加变量、表达式,也可以删除不观察的变量、表达式。
是Mscoreei.dll的正确版本吗?
在安装.NET 4.0或更高版本之后,您可能会注意到.NET进程有点不寻常。下面是用.NET 2.0编译器编译的简单“Hello World”可执行文件的加载模块的部分列表。
开始-结束模块名称
60f00000 61491000 mscorwks C:\Windows\Microsoft.NET\Framework\v2.0.50727\mscorwks.dll
6c650000 6c6b6000 mscoreei C:\Windows\Microsoft.NET\Framework\v4.0.30319\mscoreei.dll
6d420000 6d46a000 MSCOREE C:\Windows\SYSTEM32\MSCOREE.DLL
75a80000 75aca000 KERNELBASE C:\Windows\system32\KERNELBASE.dll
这里有些东西看起来不合适-mscoreei.dll文件是从v4.0.30319文件夹加载的。它在v2.0.50727的主clr dll(mscorwks.dll)旁边做什么?实际上,这是预期的行为。我们称mscoreei.dll为“shim实现”,或者简称为“shim impl”,它是.NET 4.0的新功能。上面列表中的第三个dll以前称为“shim”,现在更准确地称为“shell shim”。两者紧密地结合在一起,完成先前由mscoree单独完成的主要工作——提供加载运行时的接口。一般来说,shell shim现在由薄包装函数组成,每个薄包装函数将其功能委托给shim实现中的相应函数。
为什么要分开?作为.NET redist安装的一部分,我们已经看到大量计算机重新启动,通常是因为需要更新正在使用的文件。使用中最常见的文件是mscoree.dll,每个.NET应用程序都会加载该文件,甚至一些服务(如MSI)也会加载该文件。因此,我们进行了mscoree“拆分”,以避免机器重新启动。通过将shim实现移动到特定于版本的文件中,我们可以部署mscoreei.dll的新版本(例如,作为.NET 5.0安装的一部分,在v5.0.NET文件夹中),而无需接触计算机范围内的mscoree.dll文件。下次运行托管应用程序时,mscoree将动态查找新的mscoreei,并将其每个函数调用延迟到该mscoreei。这样,我们就可以在运行现有托管应用程序的机器上部署新版本的框架,而不需要重新启动。
请注意,mscoree总是使用它能找到的最新mscoreei,但实际加载的运行时是一个完全不同的问题。因此,看到mscoreei的新版本在同一个进程中加载了运行时dll的旧版本并不奇怪。事实上,CLR版本已经使用最新的填充程序运行多年了。如果在同时安装了.NET 1.1和.NET 2.0的计算机上运行.NET 1.1应用程序时检查文件版本,则可以看到我使用的填充程序比运行时更新。
开始-结束模块名称
79000000 79045000 mscoree C:\WINDOWS\system32\mscoree.dll
File version: 2.0.50727.42
791b0000 79412000 mscorwks C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\mscorwks.dll
File version: 1.1.4322.573
现在的区别是,shim的拆分使shim版本不总是与运行时版本匹配变得更加明显,因为版本号就在shim impl的路径中。这让我想起了大卫惠勒的一句格言:计算机科学中的所有问题都可以通过另一个层次的间接解决。
Visual Studio调试器指南---QuickWatch窗口
当处于调试模式中断下,我们可以通过QuickWatch窗口快速查看我们想观察的变量、表达式的值
打开
- 通过菜单栏方式
- 代码窗口右键方式
- 快捷键
Shift+F9
窗口使用
- Expression
这里可以输入可计算表达式,也可以选择之前观察过的表达式。 - ReeValuate
重新计算,输入表达式点此按钮会进行计算 - Add Watch
将输入的表达式添加到"Watch窗口" - Value列表
显示表达式值信息。可以显示复杂结构。
如果是一个变量,可以双击Value列修改变量值
Value列表右键菜单
菜单项 跟其他观察窗口操作一样
说明
这个窗口一次只能观察一个表达式。且这是一个模态对话框。可以显示特定时刻变量的值
CLR如何将SEH异常映射到托管异常类型
托管异常处理构建在Windows操作系统的结构化异常处理之上,通常称为SEH。这意味着CLR了解如何在SEH和托管异常系统之间进行互操作,这是一个非常关键的点,因为SEH基于异常代码的概念,而托管异常处理则表示使用托管类型的异常。CLR相应地将SEH异常映射到托管异常,具体取决于引发SEH异常的方式和引发者。
注意:下面的讨论重点是运行在Windows操作系统上的桌面CLR。虽然讨论的目的是帮助理解这个概念,但是它使用了一些将来可能会改变的实现细节来说明。
托管代码中的同步异常(Synchronous exceptions)
当托管代码使用throw关键字引发异常时,它已经实例化了一个托管异常对象,该对象将表示引发的异常。这将传递给CLR,CLR在线程上设置一些与异常相关的状态,并调用Kernel32的RaiseException API来引发托管异常。此API的第一个参数是引发异常的SEH异常代码,CLR传递0xE0434F4D(托管异常SEH代码)。
这时,操作系统进入处理场景,开始在引发异常的线程堆栈上寻找SEH异常处理程序。CLR将其函数之一注册为OS的异常处理程序,以处理托管代码引起的异常。当它看到CLR SEH异常代码时,它知道正在引发托管异常,并继续查找线程状态以检索与异常相关的详细信息(例如,标识表示引发异常的托管异常对象)。
因此,在同步托管抛出的情况下,很容易将SEH异常映射到托管异常类型。
托管代码中的异步异常(Asynchronous exceptions)
简单地说,异步异常是在没有显式抛出的情况下引发的异常。如果执行算术操作(例如除以零异常)或使用可能导致访问冲突(AV)等异常的不安全托管代码,则在托管代码中可能会发生这种情况。异步异常的有趣之处在于,它们是使用它们唯一的SEH异常代码来表示的。例如AV用0xc000005表示,除以0(整数)用0xC0000094表示,除以0(浮点数)用0xC000008E表示,这里列出了常见的异常,异常代码值可以在WinNT.h中找到。
当在托管代码中引发此类异常时,操作系统再次开始在引发异常的线程堆栈上查找异常处理程序。当调用CLR的异常处理程序时,它知道所讨论的异常不是托管代码同步抛出的,因为异常代码不是CLR SEH异常代码。因此,它不再查找与异常相关的详细信息的线程状态,而是将SEH异常映射到一个托管异常类型。例如,除以零异常(整数和浮点)使用System.DivideByZeroException表示。
同样,当在托管代码中生成真正的AV时,它的异常代码为0xc000005。由于托管代码具有空引用的概念,因此运行时很少再进行检查来确定AV是否表示尝试使用空引用。如果是,则映射到System.NullReferenceException。否则,它将映射到System.AccessViolationException类型(在v2.0和更高版本的运行时中)。