CIL“错误”条款与C#中的“catch”条款有何不同?

根据CLI标准 (Partition IIA,第19章)和System.Reflection.ExceptionHandlingClauseOptions枚举的MSDN参考页面,有四种不同的exception处理程序块:

  • catch子句: “捕获指定类型的所有对象。”
  • filter子句: “仅在filter成功时输入处理程序”。
  • 最后条款: “处理所有exception和正常退出。”
  • fault子句: “处理所有exception,但不能正常退出。”

鉴于这些简短的解释(引自CLI标准,顺便说一句),这些应该映射到C#,如下所示:

  • catchcatch (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 )。

问题:

  1. 如果catch { … }确实是一个catch子句,那么fault子句如何与catch子句不同?
  2. C#编译器是否曾输出错误条款?

有四种不同的exception处理程序块:

  • catch子句: “捕获指定类型的所有对象。”
  • filter子句: “仅在filter成功时输入处理程序”。
  • 最后条款: “处理所有exception和正常退出。”
  • fault子句: “处理所有exception,但不能正常退出。”

鉴于这些简短的解释(引自CLI标准,顺便说一句),这些应该映射到C#,如下所示:

  • catchcatch (FooException) { … }
  • filter – 在C#中不可用(但在VB.NET中为Catch FooException When booleanExpression
  • 终于finally { … }
  • 错误catch { … }

这是你出错的最后一行。 再次阅读说明。 faultfinally几乎完全相同。 它们之间的区别在于始终输入,而只有在控制通过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块可以使这些信息可用于清理代码,这样就可以干净地解决这些情况。