19

根据CLI 标准(Partition IIA,第 19 章)和System.Reflection.ExceptionHandlingClauseOptionsenum的 MSDN 参考页,有四种不同类型的异常处理程序块:

  • catch子句:“捕获指定类型的所有对象。”
  • 过滤子句:“仅当过滤成功时才输入处理程序。”
  • finally子句:“处理所有异常并正常退出。”
  • 故障子句:“处理所有异常,但不是正常退出。”

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

  • 抓住——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 { … }没有作为故障子句发出。

一个类似的实验表明,实际上发出了一个 catch 子句 ( clause.Flags == ExceptionHandlingClauseOptions.Clause),即使没有指定异常类型。

问题:

  1. 如果catch { … }真的是 catch 子句,那么故障子句与 catch 子句有何不同?
  2. C# 编译器是否曾经输出过错误子句?
4

5 回答 5

15

有四种不同类型的异常处理程序块:

  • catch子句:“捕获指定类型的所有对象。”
  • 过滤子句:“仅当过滤成功时才输入处理程序。”
  • finally子句:“处理所有异常并正常退出。”
  • 故障子句:“处理所有异常,但不是正常退出。”

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

  • 抓住——catch (FooException) { … }
  • filter — 在 C# 1中不可用(但在 VB.NET 中为Catch FooException When booleanExpression
  • 最后——finally { … }
  • 故障catch { … }

这是你出错的最后一行。再次阅读说明。fault并且finally描述几乎相同。它们之间的区别在于finally始终输入,而fault仅在控制try通过异常离开时才输入。请注意,这意味着一个catch块可能已经起作用。

如果你用 C# 写这个:

try {
    ...
} catch (SpecificException ex) {
    ...
} catch {
    ...
}

try如果控制离开via a ,则无法进入第三个块SpecificException。这就是为什么catch {}不是fault.


1由于人们在评论中不断提到这一点,a)这部分答案是对原始问题的引用,b)是的,when子句随后被添加到 C# 中。然而,它在提问时是准确的,并不是这个答案的重点。

于 2012-08-16T13:36:43.083 回答
14

.NET 异常依赖于操作系统对异常的支持。在 Windows 上称为结构化异常处理。Unix 操作系统有类似的东西,信号。

托管异常是 SEH 异常的一种非常特殊的情况。异常代码为 0xe0434f53。最后三个十六进制对拼写“COM”,告诉您一些关于 .NET 的启动方式。

一般来说,程序可能需要知道何时引发和处理任何异常,而不仅仅是托管异常。您也可以在 MSVC C++ 编译器中看到这一点。catch(...) 子句仅捕获 C++ 异常。但是,如果您使用 /EHa 选项进行编译,那么它会捕获任何异常。包括真正令人讨厌的东西,处理器异常,如访问冲突。

fault子句是 CLR的版本,它的关联块将针对任何操作系统异常执行,而不仅仅是托管异常。C# 和 VB.NET 语言不支持这一点,它们只支持托管异常的异常处理。但其他语言可能,我只知道发出它们的 C++/CLI 编译器。例如在其版本的using语句中完成,称为“堆栈语义”。

C++/CLI 支持这一点确实是有道理的,毕竟它是一种强烈支持从托管代码直接调用本机代码的语言。但不适用于 C# 和 VB.NET,它们只通过 pinvoke 编组器或 CLR 中的 COM 互操作层运行非托管代码。它已经设置了一个“catch-them-all”处理程序,将非托管异常转换为托管异常。这是您获得 System.AccessViolationException 的机制。

于 2012-08-16T13:52:34.727 回答
10

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# 编译器从不为该确切构造生成代码,而是假装它是 a catch (object) { … },这在概念上是一个 catch 子句。因此发出了一个 catch 子句。

旁注: Jeffrey Richter 的书“CLR via C#”有一些相关信息(第 472-474 页):即 CLR 允许抛出任何值,而不仅仅是Exception对象。但是,从 CLR 版本 2 开始,非Exception值会自动包装在RuntimeWrappedException对象中。因此,C# 会转换catchcatch (object)而不是catch (Exception). 然而,这是有原因的:CLR 可以被告知不要通过应用属性来包装非Exception值。[assembly: RuntimeCompatibility(WrapNonExceptionThrows = false)]

顺便说一句,VB.NET 编译器与 C# 编译器不同,它转换CatchCatch anonymousVariable As Exception.


2. C# 编译器是否曾经输出过错误子句?

它显然不会为catch { … }. 但是,Bart de Smet 的博客文章“读者挑战 – C# 中的错误处理程序”表明 C# 编译器在某些情况下确实会产生错误子句。

于 2012-08-16T13:13:32.760 回答
8

正如人们所指出的,一般来说 C# 编译器不会生成错误处理程序。但是,stakx 链接到Bart de Smet 的关于如何让 C# 编译器生成故障处理程序的博客文章。

C# 确实使用错误处理程序来实现迭代器块内的 using 语句。例如,以下 C# 代码将导致编译器使用错误子句:

public IEnumerable<string> GetSomeEnumerable()
{
    using (Disposable.Empty)
    {
        yield return DoSomeWork();
    }
}

使用 dotPeek 和“显示编译器生成的代码”选项反编译生成的程序集,可以看到错误子句:

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 块,这对迭代器块没有意义 - try/finally 将在生成第一个值之后 Dispose。

但如果 DoSomeWork 抛出异常,您确实需要 Dispose。所以故障处理程序在这里很有用。它只会在发生异常的情况下调用 Dispose,并允许异常冒泡。从概念上讲,这类似于处理然后重新抛出的 catch 块。

于 2013-06-05T01:12:06.540 回答
4

故障块相当于说:

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 有所不同。除此之外,通过上述实现,如果发生异常并且堆栈跟踪正在报告行号,它将包括...do stuff发生异常的行号。相比之下,当使用 catch-and-rethrow 时,堆栈跟踪将报告重新抛出的行号。如果...do stuff包含两个或多个对 的调用foo,并且其中一个调用引发异常,则知道失败调用的行号可能会有所帮助,但 catch-and-rethrow 会丢失该信息。

上述实现的最大问题是必须手动添加success = true;代码中可能退出try块的每个位置,并且finally块无法知道可能存在哪些异常。如果我有我的 druthers,会有一条finally (Exception ex)语句设置Ex为导致 try 块退出的异常(或null如果块正常退出)。这不仅消除了手动设置“成功”标志的需要,而且可以合理地处理清理代码中发生异常的情况。在这种情况下,不应该掩盖清理异常(即使原始异常通常代表调用代码所期望的条件,清理失败也可能代表它不是的条件),但可能不想丢失原始异常(因为它可能包含有关清理失败原因的线索)。允许一个finally块知道它为什么被输入,并包括一个扩展版本IDisposable,一个块可以通过它using使这些信息可用于清理代码,这将使干净地解决这种情况成为可能。

于 2012-08-16T15:56:33.197 回答