I was speaking about Delegates at a user group a couple weeks ago and decided it was time to dive a little bit deeper into exception handling with Multi-Cast Delegates. As a reminder about how multi-cast delegates work (and what happens if one of the methods throws an exception), check out my previous article: Exceptions in Multi-Cast Delegates.
Standard Behavior
As a quick review, all delegates in .NET are multi-cast delegates, meaning that multiple methods can be assigned to a single delegate. When that delegate is invoked, all of the methods get run synchronously in the order that they were assigned.
Here are the delegate assignments that we were looking at before:
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));
When the delegate is invoked normally, the execution stops with the exception in the second method. The third method never gets run. (Check the previous article for details.)
The Issue
The main issue is that we are rarely on "both sides" of the delegate. Usually we are either creating methods to assign to a delegate (to hook into an existing framework or API), or we are creating an API with a delegate for other people to use.
Think of it from this point of view: Let's assume that we have a publish/subscribe scenario (after all, events are just special kinds of delegates). In that case, we have the publisher (the API providing the delegate to which we can assign methods) and the subscribers (the methods that are assigned).
If we are creating subscriber methods, then the solution is easy. We just need to make sure that we don't let any unhandled exception escape our method. If our code is not working correctly, then we need to trap the error and make sure that it doesn't impact any other subscribers.
But things are a little more complicated if we are the publisher. As we saw in the previous examples, if there is a misbehaving subscriber, then other subscribers may be impacted. We need to avoid this. If we are responsible for the API, then we need to ensure that we notify every subscriber, regardless of whether they are behaving or not.
A More Robust Solution
We've already seen that the code we had previously just won't cut it:
try
{
act(people);
}
catch (Exception ex)
{
Console.WriteLine("Caught Exception: {0}", ex.Message);
}
To fix this, we can dig a little deeper into the delegate itself. It turns out that all delegates have a method called "GetInvocationList()". This will give us a collection of all of the methods that are assigned to the delegate (the return is technically an array of Delegate objects). If there are no methods, then the list will be empty; otherwise, it will have all of the methods assigned. This gives us a chance to handle things a little more manually.
foreach (var actDelegate in act.GetInvocationList())
{
try
{
actDelegate.Method.Invoke(this, new object[] { people });
}
catch
{
Console.WriteLine("Error in delegate method.");
}
}
Let's quickly walk this code. First we call GetInvocationList() on our delegate and then "foreach" through it. As noted above, each item (actDelegate) will be a System.Delegate object.
Now, inside the loop, we can access each method individually. The Delegate has a Method property that gives us a MethodInfo object. MethodInfo has an Invoke method that allows us to run the delegate method. The syntax here looks a bit odd. The first parameter is the object on which to invoke the method. In our case, we use "this" because our delegate exists in the class instance that we are in.
The next parameter is an object array with all of the parameters that will be passed to the method being invoked. In our case, we have a single parameter (List<Person>). So, we just have to create a new object array with that parameter as a member.
This seems like a lot of work, but because we are invoking each method individually, we can wrap each invocation in a try/catch block (which is exactly what we did). Now, if a method misbehaves, we simply catch the exception and move on to the next one in the list.
If we were to use our sample assignments above, we get the following output:
6.42857142857143
Error in delegate method.
10/17/1975 12:00:00 AM
So, we've successfully coded around a misbehaving method and ensured that all of the methods assigned to our delegate are run.
Is This Really Necessary?
The question on the top of your mind is "Is this really necessary?" After all, mutli-cast delegates are designed to allow us to make a single invocation that runs all of the assigned methods. Should we add this much complexity?
The answer is "It depends." (You've probably noticed that "it depends" is my standard answer.) If I were writing a delegate where I control both ends (for example, I'm writing both the publisher and the subscribers), then I probably wouldn't code this way. I would put more effort into ensuring that the subscribers behave appropriately and would not throw an unhandled exception.
However, if I were creating an API for general distribution, I would be much more protective about my delegate invocations. In that scenario, we need to make sure that a single bad input cannot bring down the system.
Wrap Up
Delegates are extremely powerful and useful -- multi-cast delegates even more so. They can help us make our system extensible without requiring modification. They can offer "hooks" for other developers to run their own code. They give us a way to do pseudo-functional programming in our object-oriented C# environment.
We need to be aware of what can go wrong when we add these tools. As we've seen, it is not difficult to handle exceptions (either in the methods or in the delegate invocation). When we make sure that we have our bases covered, then we can minimize unpleasant surprises in our system.
Happy Coding!
No comments:
Post a Comment