Exception handling (contradicting documentation / try-finally vs. using)

生来就可爱ヽ(ⅴ<●) 提交于 2020-12-08 07:07:58

问题


I thought I had understood how exception handling in C# works. Re-reading the documentation for fun and self-confidence, I have run into problems:

This document claims that the following two code snippets are equivalent, even more, that the first one is translated to the latter one at compile time.

using (Font font1 = new Font("Arial", 10.0f)) {
  byte charset = font1.GdiCharSet;
}

and

{
  Font font1 = new Font("Arial", 10.0f);
  try {
    byte charset = font1.GdiCharSet;
  }
  finally {
    if (font1 != null)
      ((IDisposable)font1).Dispose();
  }
}

Furthermore, it claims:

The using statement ensures that Dispose is called even if an exception occurs while you are calling methods on the object.

In contrast, that document states:

Within a handled exception, the associated finally block is guaranteed to be run. However, if the exception is unhandled, execution of the finally block is dependent on how the exception unwind operation is triggered.

I do not get this together. In the code example from the first document, the exception clearly is unhandled (since there is no catch block). Now, if the statement from the second document is true, the finally block is not guaranteed to execute. This ultimately contradicts what the first document says ("The using statement ensures ...") (emphasis mine).

So what is the truth?

EDIT 1

I still don't get it. StevieB's answer has made me read more parts from the C# language specification. In section 16.3, we have:

[...] This search continues until a catch clause is found that can handle the current exception [...] Once a matching catch clause is found, the system prepares to transfer control to the first statement of the catch clause. Before execution of the catch clause begins, the system first executes, in order, any finally clauses that were associated with try statements more nested that than the one that caught the exception.

So have I made a simple test program which contains code which produces a division by zero and is within a try block. That exception is never caught in any of my code, but the respective try statement has a finally block:

int b = 0;

try {
  int a = 10 / b;
}
finally {
  MessageBox.Show("Hello");
}

Initially, according to the documentation snippet above, I had expected that the finally block never would be executed and that the program just would die when being executed without a debugger attached. But this is not the case; instead, the "exception dialog box" we all know too well is shown, and after that, the "Hello" dialog box appears.

After thinking a while about that and after having read docs, articles and questions like this and that, it became clear that this "exception dialog box" is produced by a standard exception handler which is built into Application.Run() and the other usual methods which could "start" your program, so I am not wondering any more why the finally block is run.

But I am still totally baffled because the "Hello" dialog appears after the "exception dialog box". The documentation snippet above is pretty clear (well, probably I am just too silly again):

The CLR won't find a catch clause which is associated with the try statement where the division by zero happens. So it should pass the exception up one level to the caller, won't find a matching catch clause there as well (there is not even a try statement there) and so on (as noted above, I do not handle (i.e. catch) any exception in this test program).

Finally, the exception should meet the CLR's default catch-all exception handler (i.e. that one which is by default active in Application.Run() and its friends), but (according to the documentation above) the CLR should now execute all finally blocks which are more deeply nested than that default handler ("my" finally block belongs to these, doesn't it?) before executing the CLR catch-all default handler's catch block.

That means that the "Hello" dialog should appear before the "exception dialog box", doesn't it?. Well, obviously, it's the other way around. Could somebody elaborate on that?


回答1:


This document claims that the following two code snippets are equivalent

They are.

The using statement ensures that Dispose is called even if an exception occurs while you are calling methods on the object.

Pretty much.

This ultimately contradicts what the first document says

Well, the first one was being a bit too vague, rather than flat-out incorrect.

There are cases where a finally will not run, including that implied by a using. A StackOverflowException would be one example (a real one from overflowing the stack, if you just do throw new StackOverflowException() the finally will run).

All the examples are things you can't catch, and your application is going down, so if the clean up from using is only important while the application is running, then finally is fine.

If the clean-up is vital even when the program crashes, then finally can never be enough, as it can't deal with e.g. a power plug being pulled out, which in the sort of cases where clean up is vital even in a crash, is a case that needs to be considered.

In any case where the exception is caught further up and the program continues, the finally will run.

With catchable exceptions that aren't caught, then finally blocks will generally run, but there are still some exceptions. One would be if the try-finally was inside a finaliser and the try took a long time; after a while on the finaliser queue the application will just fail-fast.




回答2:


If your definition of "unhandled exception" (that it must be handled by a catch clause within the same try block) was correct, there would be no reason to ever allow the construct

try {
   ...
}
finally {
   ...
}

Since by your definition, the finally block would never run. Since the above construct is valid, we must conclude that your definition of "unhandled exception" is incorrect.

What it means is "if the exception is not handled by any exception handler, anywhere in the call stack".




回答3:


You have to give the documentation a little leeway. In most circumstances there's an implied "within reason". For example, if I turn my computer off none of those finally blocks or using/Disposes will be called.

Apart from the computer shutting off, there are a handful of circumstances that OS can terminate your application--effectively shutting it off. In these circumstances Dispose and finally won't be invoked. Those are usually pretty serious error conditions like out of stack or out of memory. There are some complex scenarios where that can happen with less-than-exceptional-exceptions. For example, if you have native code that creates a managed object on a background thread and that managed object and something on that thread throws an native exception, it's likely that the managed exception handlers won't get called (e.g. Dispose) because the OS will just terminate the thread and anything that could have been disposed is not accessible any more.

But, yes, those statements are effectively equivalent and within reason the finally block will be executed and Dispose will be called.




回答4:


The C# language specification states that the finally blocks will be executed (be that in a using statement or elsewhere) for System.Exception or any of its derived exceptions. Of course, if you get an exception that can't be handled with the usual try..catch logic e.g. AccessViolationException all bets are off which is where the ambiguity comes in.

The spec is installed with Visual Studio 2013 and later - with 2017 it's in C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\VC#\Specifications\1033

In section

8.9.5 The throw statement

we see the following:

When an exception is thrown, control is transferred to the first catch clause in an enclosing try statement that can handle the exception. The process that takes place from the point of the exception being thrown to the point of transferring control to a suitable exception handler is known as exception propagation. Propagation of an exception consists of repeatedly evaluating the following steps until a catch clause that matches the exception is found. In this description, the throw point is initially the location at which the exception is thrown.

  • In the current function member, each try statement that encloses the throw point is examined. For each statement S, starting with the innermost try statement and ending with the outermost try statement, the following steps are evaluated:
    • If the try block of S encloses the throw point and if S has one or more catch clauses, the catch clauses are examined in order of appearance to locate a suitable handler for the exception. The first catch clause that specifies the exception type or a base type of the exception type is considered a match. A general catch clause (§8.10) is considered a match for any exception type. If a matching catch clause is located, the exception propagation is completed by transferring control to the block of that catch clause.
    • Otherwise, if the try block or a catch block of S encloses the throw point and if S has a finally block, control is transferred to the finally block. If the finally block throws another exception, processing of the current exception is terminated. Otherwise, when control reaches the end point of the finally block, processing of the current exception is continued.
  • If an exception handler was not located in the current function invocation, the function invocation is terminated, and one of the following occurs:
    • If the current function is non-async, the steps above are repeated for the caller of the function with a throw point corresponding to the statement from which the function member was invoked.
    • If the current function is async and task-returning, the exception is recorded in the return task, which is put into a faulted or cancelled state as described in §10.14.1.
    • If the current function is async and void-returning, the synchronization context of the current thread is notified as described in §10.14.2.
  • If the exception processing terminates all function member invocations in the current thread, indicating that the thread has no handler for the exception, then the thread is itself terminated. The impact of such termination is implementation-defined.



回答5:


I believe (correct me if I am wrong) the answer is lying in the definition:

The using statement ensures that Dispose is called even if an exception occurs while you are calling methods on the object.

So if any exceptions occur in any methods called by the object of using, finally is ensured to run. On the other hand finally is not guaranteed to run if inside the using block call other methods not related with the object cause an exception




回答6:


First of all, I would like to bring to your notice that using statement cannot be used for all types. This can be used only for types that implement IDisposable interface which has a functionality to dispose the object automatically. This is present in the second document you mentioned

C# also contains the using statement, which provides similar functionality for IDisposable objects in a convenient syntax.

This means if an unhandled exception happens, cleaning up of your objects is handled by the Dispose() method of the type(this is given for using statement documentation)

Coming to the query, even though your finally block(generated) is not guarenteed to run for unhandled exceptions, the object dispose operation is handled during runtime by .Net CLR

Hope this clears your doubt



来源:https://stackoverflow.com/questions/42250582/exception-handling-contradicting-documentation-try-finally-vs-using

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!