CIL“错误”条款与C#中的“catch”条款有何不同?
根据CLI标准 (Partition IIA,第19章)和System.Reflection.ExceptionHandlingClauseOptions
枚举的MSDN参考页面,有四种不同的exception处理程序块:
- catch子句: “捕获指定类型的所有对象。”
- filter子句: “仅在filter成功时输入处理程序”。
- 最后条款: “处理所有exception和正常退出。”
- fault子句: “处理所有exception,但不能正常退出。”
鉴于这些简短的解释(引自CLI标准,顺便说一句),这些应该映射到C#,如下所示:
- catch –
catch (FooException) { … }
- filter – 在C#中不可用(但在VB.NET中为
Catch FooException When booleanExpression
) - 终于 –
finally { … }
- 错误 –
catch { … }
实验:
一个简单的实验表明,这种映射不是.NET的C#编译器真正做的事情:
// using System.Linq; // using System.Reflection; static bool IsCatchWithoutTypeSpecificationEmittedAsFaultClause() { try { return MethodBase .GetCurrentMethod() .GetMethodBody() .ExceptionHandlingClauses .Any(clause => clause.Flags == ExceptionHandlingClauseOptions.Fault); } catch // <-- this is what the above code is inspecting { throw; } }
此方法返回false
。 也就是说, catch { … }
尚未作为错误条款发出。
类似的实验表明,事实上,即使没有指定exception类型,也会发出catch子句( clause.Flags == ExceptionHandlingClauseOptions.Clause
)。
问题:
- 如果
catch { … }
确实是一个catch子句,那么fault子句如何与catch子句不同? - C#编译器是否曾输出错误条款?
有四种不同的exception处理程序块:
- catch子句: “捕获指定类型的所有对象。”
- filter子句: “仅在filter成功时输入处理程序”。
- 最后条款: “处理所有exception和正常退出。”
- fault子句: “处理所有exception,但不能正常退出。”
鉴于这些简短的解释(引自CLI标准,顺便说一句),这些应该映射到C#,如下所示:
- catch –
catch (FooException) { … }
- filter – 在C#中不可用(但在VB.NET中为
Catch FooException When booleanExpression
)- 终于 –
finally { … }
- 错误 –
catch { … }
这是你出错的最后一行。 再次阅读说明。 fault
, finally
几乎完全相同。 它们之间的区别在于始终输入,而只有在控制通过exception离开try
时才输入fault
。 请注意,这意味着catch
块可能已经起作用。
如果你用C#写这个:
try { ... } catch (SpecificException ex) { ... } catch { ... }
如果控件通过SpecificException
离开try
,则无法输入第三个块。 这就是为什么catch {}
不是fault
映射的原因。
.NETexception捎带到操作系统对exception的支持上。 在Windows上称为结构化exception处理。 Unix操作系统有类似的信号。
托管exception是SEHexception的一个非常具体的案例。 exception代码是0xe0434f53。 最后三个hex对拼写“COM”,告诉你一些关于.NET开始的方式。
一般而言,程序可能会知道何时引发和处理任何exception,而不仅仅是托管exception。 您也可以在MSVC C ++编译器中看到这一点。 catch(…)子句只捕获C ++exception。 但是如果使用/ EHa选项进行编译,则会捕获任何exception。 包括真正令人讨厌的东西,处理器exception,如访问违规。
fault子句是CLR的版本,它的关联块将针对任何操作系统exception执行,而不仅仅是托管exception。 C#和VB.NET语言不支持此function,它们仅支持托管exception的exception处理。 但是其他语言可能,我只知道发出它们的C ++ / CLI编译器。 例如,在其using语句的版本中,称为“堆栈语义”。
确实C ++ / CLI支持它,它毕竟是一种强烈支持从托管代码直接调用本机代码的语言。 但是对于C#和VB.NET,它们只能通过pinvoke marshaller或CLR中的COM互操作层运行非托管代码。 其中已经建立了一个“捕获所有”的处理程序,可以将非托管exception转换为托管exception。 这是获取System.AccessViolationException的机制。
1.如果
catch { … }
确实是一个catch子句,那么fault子句如何与catch子句不同?
C#编译器(至少是.NET附带的编译器)实际上似乎是编译catch { … }
,好像它真的是catch (object) { … }
。 这可以使用下面的代码显示。
// using System; // using System.Linq; // using System.Reflection; static Type GetCaughtTypeOfCatchClauseWithoutTypeSpecification() { try { return MethodBase .GetCurrentMethod() .GetMethodBody() .ExceptionHandlingClauses .Where(clause => clause.Flags == ExceptionHandlingClauseOptions.Clause) .Select(clause => clause.CatchType) .Single(); } catch // <-- this is what the above code is inspecting { throw; } }
该方法返回typeof(object)
。
从概念上讲,故障处理程序类似于 catch { … }
; 但是,C#编译器从不为该确切的构造生成代码,但假装它是一个catch (object) { … }
,它在概念上是一个catch子句。 因此会释放一个catch子句。
旁注: Jeffrey Richter的书“CLR via C#”有一些相关信息(第472-474页):即CLR允许抛出任何值,而不仅仅是
Exception
对象。 但是,从CLR版本2开始,非Exception
值将自动包装在RuntimeWrappedException
对象中。 因此,C#将catch
转换为catch (object)
而不是catch (Exception)
似乎有点令人惊讶。 但是有一个原因:可以通过应用[assembly: RuntimeCompatibility(WrapNonExceptionThrows = false)]
属性来告诉CLR不要包装非Exception
值。顺便说一句,与C#编译器不同,VB.NET编译器将
Catch
转换为Catch anonymousVariable As Exception
。
2. C#编译器是否曾输出错误条款?
它显然不会为catch { … }
发出错误条款。 但是,Bart de Smet的博客文章“读者挑战 - C#中的error handling程序”表明C#编译器在某些情况下会产生错误条款。
正如人们所指出的,一般来说C#编译器不会生成error handling程序。 但是,stakx与Bart de Smet的博客文章有关如何让C#编译器生成error handling程序。
C#确实使用error handling程序来实现迭代器块内的语句。 例如,以下C#代码将导致编译器使用fault子句:
public IEnumerable GetSomeEnumerable() { using (Disposable.Empty) { yield return DoSomeWork(); } }
使用dotPeek和“显示编译器生成的代码”选项对生成的程序集进行反编译,可以看到fault子句:
bool IEnumerator.MoveNext() { try { switch (this.<>1__state) { case 0: this.<>1__state = -1; this.<>7__wrap1 = Disposable.Empty; this.<>1__state = 1; this.<>2__current = this.<>4__this.DoSomeWork(); this.<>1__state = 2; return true; case 2: this.<>1__state = 1; this.<>m__Finally2(); break; } return false; } __fault { this.System.IDisposable.Dispose(); } }
通常,using语句将映射到try / finally块,这对于迭代器块没有意义 – 在生成第一个值之后尝试/ finally将Dispose。
但是如果DoSomeWork抛出exception,你确实想要Dispose。 所以故障处理程序在这里很有用。 它只会在发生exception的情况下调用Dispose,并允许exception冒泡。 从概念上讲,这类似于处理然后重新抛出的catch块。
故障块相当于说:
bool success; try { success = false; ... do stuff success = true; // Also include this immediately before any 'return' } finally { if (!success) { ... do "fault" stuff here } }
请注意,这与catch-and-rethrow在语义上有所不同。 除此之外,通过上面的实现,如果发生exception并且堆栈跟踪报告行号,它将包括...do stuff
发生exception的行的数量。 相反,当使用catch-and-rethrow时,堆栈跟踪将报告重新抛出的行号。 如果...do stuff
包含对foo
两次或多次调用,并且其中一个调用抛出exception,知道失败的调用的行号可能会有所帮助,但catch-and-rethrow会丢失该信息。
上述实现的最大问题是必须手动添加success = true;
到代码中可能退出try
块的每个地方,并且finally
块无法知道哪些exception可能正在等待。 如果我有我的druthers,会有一个finally (Exception ex)
语句将Ex
设置为导致try块退出的exception(如果块正常退出则为null
)。 这不仅消除了手动设置’success’标志的需要,而且还允许合理处理清理代码中发生exception的情况。 在这种情况下,不应该模糊清理exception(即使原始exception通常表示调用代码期望的条件,清理失败可能代表它不是一个条件)但是人们可能不想丢失原始exception(因为它可能包含清理失败原因的线索)。 允许finally
块知道输入的原因,并包括IDisposable
的扩展版本, using
块可以使这些信息可用于清理代码,这样就可以干净地解决这些情况。