问题
According to the CLI standard (Partition IIA, chapter 19) and the MSDN reference page for the System.Reflection.ExceptionHandlingClauseOptions enum, there are four different kinds of exception handler blocks:
- catch clauses: "Catch all objects of the specified type."
- filter clauses: "Enter handler only if filter succeeds."
- finally clauses: "Handle all exceptions and normal exit."
- fault clauses: "Handle all exceptions but not normal exit."
Given these brief explanations (cited from the CLI Standard, btw.), these should map to C# as follows:
- catch —
catch (FooException) { … }
- filter — not available in C# (but in VB.NET as
Catch FooException When booleanExpression
) - finally —
finally { … }
- fault —
catch { … }
Experiment:
A simple experiment shows that this mapping is not what .NET's C# compiler really does:
// 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;
}
}
This method returns false
. That is, catch { … }
has not been emitted as a fault clause.
A similar experiment shows that in fact, a catch clause was emitted (clause.Flags == ExceptionHandlingClauseOptions.Clause
), even though no exception type has been specified.
Questions:
- If
catch { … }
really is a catch clause, then how are fault clauses different from catch clauses? - Does the C# compiler ever output fault clauses at all?
回答1:
there are four different kinds of exception handler blocks:
- catch clauses: "Catch all objects of the specified type."
- filter clauses: "Enter handler only if filter succeeds."
- finally clauses: "Handle all exceptions and normal exit."
- fault clauses: "Handle all exceptions but not normal exit."
Given these brief explanations (cited from the CLI Standard, btw.), these should map to C# as follows:
- catch —
catch (FooException) { … }
- filter — not available in C# (but in VB.NET as
Catch FooException When booleanExpression
)- finally —
finally { … }
- fault —
catch { … }
It's that last line where you went wrong. Read the descriptions again. fault
and finally
are described practically identically. The difference between them is that finally
is always entered, whereas fault
is only entered if control leaves the try
via an exception. Note that this means that a catch
block may have already acted.
If you write this in C#:
try {
...
} catch (SpecificException ex) {
...
} catch {
...
}
Then there is no way that the third block will be entered if control leaves the try
via a SpecificException
. That's why catch {}
isn't a mapping for fault
.
回答2:
.NET exceptions piggy-back onto the operating system's support for exceptions. Called Structured Exception Handling on Windows. Unix operating systems have something similar, signals.
A managed exception is a very specific case of an SEH exception. The exception code is 0xe0434f53. The last three hex pairs spell "COM", tells you something about the way .NET got started.
A program in general might have a stake at knowing when any exception is raised and handled, not just managed exceptions. You can see this back in the MSVC C++ compiler as well. A catch(...) clause only catches C++ exceptions. But if you compile with the /EHa option then it catches any exception. Including the really nasty stuff, processor exceptions like access violations.
The fault clause is the CLR's version of that, its associated block will execute for any operating system exception, not just managed ones. The C# and VB.NET languages do not support this, they only support exception handling for managed exceptions. But other languages may, I only know of the C++/CLI compiler emitting them. Done for example in its version of the using statement, called "stack semantics".
It does make sense that C++/CLI would support that, it is after all a language that strongly supports directly calling native code from managed code. But not for C# and VB.NET, they only ever run unmanaged code through the pinvoke marshaller or the COM interop layer in the CLR. Which already sets up a 'catch-them-all' handler that translates unmanaged exceptions into managed ones. Which is the mechanism by which you get a System.AccessViolationException.
回答3:
1. If
catch { … }
really is a catch clause, then how are fault clauses different from catch clauses?
The C# compiler (at least the one that ships with .NET) actually appears to compile catch { … }
as if it were really catch (object) { … }
. This can be shown with the code below.
// 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;
}
}
That method returns typeof(object)
.
So conceptually, a fault handler is is similar to a catch { … }
; however, the C# compiler never generates code for that exact construct but pretends that it is a catch (object) { … }
, which is conceptually a catch clause. Thus a catch clause gets emitted.
Side note: Jeffrey Richter's book "CLR via C#" has some related information (on pp. 472–474): Namely that the CLR allows any value to be thrown, not just
Exception
objects. However, starting with CLR version 2, non-Exception
values are automatically wrapped in aRuntimeWrappedException
object. So it seems somewhat surprising that C# would transformcatch
intocatch (object)
instead ofcatch (Exception)
. There is however a reason for this: The CLR can be told not to wrap non-Exception
values by applying a[assembly: RuntimeCompatibility(WrapNonExceptionThrows = false)]
attribute.By the way, the VB.NET compiler, unlike the C# compiler, translates
Catch
toCatch anonymousVariable As Exception
.
2. Does the C# compiler ever output fault clauses at all?
It obviously doesn't emit fault clauses for catch { … }
. However, Bart de Smet's blog post "Reader challenge – fault handlers in C#" suggests that the C# compiler does produce fault clauses in certain circumstances.
回答4:
As people have pointed out, generally speaking the C# compiler doesn't generate fault handlers. However, stakx linked to Bart de Smet's blog post on how to get the C# compiler to generate a fault handler.
C# does use fault handlers to implement using statements that are inside iterator blocks. Eg the following C# code will cause the compiler to use a fault clause:
public IEnumerable<string> GetSomeEnumerable()
{
using (Disposable.Empty)
{
yield return DoSomeWork();
}
}
Decompiling the generated assembly with dotPeek and the "Show compiler-generated code" option on, you can see the fault clause:
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();
}
}
Where normally a using statement would map to a try/finally block, this doesn't make sense for iterator blocks - a try/finally would Dispose after the first value is generated.
But if DoSomeWork throws an exception, you do want to Dispose. So a fault handler is useful here. It will call Dispose only in the case where an exception occurs, and allow the exception to bubble up. Conceptually this is similar to a catch block that disposes and then re-throws.
回答5:
A fault block would be equivalent to saying:
bool success;
try
{
success = false;
... do stuff
success = true; // Also include this immediately before any 'return'
}
finally
{
if (!success)
{
... do "fault" stuff here
}
}
Note that this is somewhat different semantically from catch-and-rethrow. Among other things, with the implementation above, if an exception occurs and the stack trace is reporting line numbers, it will include the number of the line in ...do stuff
where the exception occurred. By contrast, when using catch-and-rethrow, the stack trace would report the line number of the rethrow. If ...do stuff
includes two or more calls to foo
, and one of those calls throws an exception, knowing the line number of the call that failed could be helpful, but catch-and-rethrow would lose that information.
The biggest problems with the above implementation are that one must manually add success = true;
to every place in the code that could exit the try
block, and that there's no way for the finally
block to know what exception may be pending. If I had my druthers, there would be a finally (Exception ex)
statement which would set Ex
to the exception that caused the try block to exit (or null
if the block exited normally). Not only would that eliminate the need to manually set the 'success' flag, but it would allow sensible handling of the case where an exception occurs in cleanup code. In such a situation, one should not obscure the cleanup exception (even if the original exception would normally represent a condition the calling code was expecting, the cleanup failure probably represents a condition it wasn't) but one probably doesn't want to lose the original exception either (since it probably contains clues as to why the cleanup failed). Allowing a finally
block to know why it was entered, and including an extended version of IDisposable
via which a using
block could make such information available to cleanup code, would make it possible to resolve such situations cleanly.
来源:https://stackoverflow.com/questions/11987953/how-are-cil-fault-clauses-different-from-catch-clauses-in-c