At the last 2 code camps (So Cal Code Camp and Desert Code Camp), the same question came up. How do exceptions affect the process flow when invoking a multi-cast delegate? Let's do a quick review and then take a closer look. For more information, please take a look at Get Func<>-y: Delegates in .NET.
Multi-Cast Delegates
All delegates in .NET are multi-cast delegates. This means that we can assign multiple methods to the same delegate variable. When we invoke the delegate variable (to run the methods), then all methods that have been assigned are run.
For example, take a look at this Action variable:
Action<List<Person>> act;
Then we can assign multiple methods as follows (in this case using lambda expressions):
act += p => Console.WriteLine(p.Average(r => r.Rating)).ToString();
act += p => Console.WriteLine(p.Min(s => s.StartDate));
Then we can invoke the delegate (assuming that "people" is a list of Person objects):
act(people);
The resulting output to the console window is as follows:
6.4285714285714288
10/17/1975
Single-Threaded Execution
.NET does not do anything magical when it runs the methods assigned to the delegate. They simply run in order, synchronously, on the same thread where the delegate is invoked. In the examples from the "Get Func<>-y" session, we invoke the delegate from the UI thread of a WPF application, and we see that if one of the methods is blocking (such as a MessageBox.Show() method that waits for user input), then the other methods do not execute until that method has completed. (As you can imagine, if we wanted the methods to run on separate threads, we could do that; but that is not the default behavior.)
Exceptions
Now, what happens if we add an exception into the mix:
act += p => Console.WriteLine(p.Average(r => r.Rating)).ToString();
act += p => { throw new Exception("Error Here"); };
act += p => Console.WriteLine(p.Min(s => s.StartDate));
What do we expect to happen? Will all 3 methods execute? None of the methods? Only the first one?
The answer turns out to be pretty simple. Since the methods are run in order, the first method will execute successfully, the second method will throw an exception, and the third will not execute. Since we do not have any exception handling defined, our application will stop working. The console window will have the following:
6.4285714285714288
So, we can see that the first method did in fact run, but the third method did not.
Exception Handling
We can keep our application operational by adding a try/catch block around the invocation of the delegate. This will keep our application from completely erroring out:
try
{
act(people);
}
catch (Exception ex)
{
Console.WriteLine("Caught Exception: {0}", ex.Message);
}
And our output now looks like the following:
6.4285714285714288
Caught Exception: Error Here
But something to notice, even though our application continues (and completes normally), the third method still does not run. And this should not be surprising. When an exception is thrown, the system will walk up the call stack until it finds an exception handler. If it does not find one, then the application will error out. In this case, when the exception in the second method is thrown, it walks back up the call stack to the method that invoked the delegate. There it finds the try/catch and handles the exception. But since we exited out of the actual delegate invocation, the third method is not called.
[Update: For a more robust solution, see More Delegate Exception Handling.]
Some Things to Think About
Now let's consider a bit how we use delegates. Generally, we (as developers) are on one end of the delegate or the other -- meaning we are creating a delegate so that other developers can hook into our code, or we are using a delegate to hook into someone else's code.
If we are creating a delegate that we invoke (such as the call to act(people) above), then we should definitely consider wrapping the invocation in a try/catch block. When someone assigns a method to a delegate that we then invoke, we really have no control over what that assigned method does. This means that we need to protect our code to make sure that someone else's mistake does not cause our code to crash. We need to handle any exceptions that are generated by the invocation so that our application can continue to operate (or at least fail gracefully).
Also, as we noted in "Get Func<>-y", we should be checking the delegate before invocation to make sure that it is not null. In this code sample, it would look like the following:
try
{
if(act != null)
act(people);
}...
If we try to invoke a delegate variable that does not have any methods assigned, then we will get a null reference exception. Alternately, we could handle the NullReferenceException in our catch block, but it's usually better to avoid the exception if it can be anticipated.
But what if we're on the other side of the delegate? What if we are a developer who is assigning methods to a delegate to hook into someone else's code? In that case, we need to follow standard practices for error handling in those methods. We could easily add a try/catch block to the method that we assign to a delegate variable. This way we would have the first shot at handling the exception. And since the exception is happening in our code, we are probably in the best position to try to handle it. If we cannot handle the exception, then we can let it bubble up (as we saw in the sample above). How we handle this will depend on the type of delegates that we are working with, what our business processes are, and how we have designed our error handling and failure modes through the rest of the application.
We always need to be on the lookout for the unexpected. In the sample code that shows up in my demos, the error handling has been excluded (just so that we can more clearly focus on the topic at hand). But when building our applications, we need to make sure that we are following good programming practices and making sure that we can handle the exceptions that may occur in our code.
Happy Coding!
No comments:
Post a Comment