Imagine that you have two classes, Caller and Notifier. Your design stipulates that the worker class Notifier should do some work on behalf of Caller and notify Caller (by calling a method named Notify) when that work is completed. This is a common-enough problem in programming, especially in a situation where another class spawns a new thread to do work asynchronously on behalf of another. The .NET UI libraries need to do this sort of thing all the time to notify your code of UI events.
Let’s take a look at three possible solutions to this problem. For the sake of simplicity, we’ll be leaving out any thread creation from the examples—just imagine that they are there.
A reference-based solution
The easiest way to solve a cross-class communication problem would be to write Notifier in such a way that it maintains a private reference to Caller. Notifier can then call Caller.Notify as soon as it completes the requested work. Check out Listing A for this solution in C#.
You’ll see that the Notifier constructor accepts a single parameter, of type Caller, and saves a reference to that instance of Caller as a private variable. Once Caller invokes Notifier.DoSomething, Notifier performs a task, in this case writing some text to the console, and then invokes the Notify method on its private Caller instance.
Holding a private reference is the simplest way to provide cross-class communication, and it works fairly well. There’s a problem, though: The two classes are very tightly coupled. What happens if another class, unrelated to Caller, wants to use Notifier?
An interface-based solution
Having Caller implement an interface provides a better solution. In Listing B, you’ll see that I’ve rewritten both Caller and Notifier and added a NotifyMe interface, which has a single method: Notify. In this solution, Caller implements NotifyMe and sends a reference to itself (cast as type NotifyMe) to Notifier via the latter’s constructor. When Notifier has finished the work it does in DoSomething, it calls NotifyMe.Notify, and Caller handles the method call.
Relying on a common interface certainly beats our first solution. The two classes need to know very little about each other, and any class that implements the agreed-upon interface can receive a message from the worker class. However, suppose you're dealing with two classes that have no common interface, or a caller that needs to be notified on a static method? To throw yet another monkey wrench into things, what if you need to notify multiple methods, or even multiple classes?
Enter the delegate
If you happen to be a C++ developer, you’ve probably been shouting out the words “function pointer” for the last few paragraphs. For the uninitiated, a function pointer is a pointer to the address holding the entry point of a function. At runtime, the address can be dereferenced and the function can be executed without the caller knowing the exact name of the function in question. Function pointers are powerful constructs, but they suffer from two problems:
- · They generally aren’t type safe, meaning that a mismatched parameter type, missing parameter, or incorrect return type can’t be detected before runtime.
- · In a managed memory environment, like .NET, things are routinely moved around in memory in the interests of optimizing performance. That address you have may have been valid a few moments ago when it was handed to you, but that block of memory could have moved in the meantime. For all you know, you could now be holding a pointer to a peanut butter sandwich.
The .NET Framework provides a type-safe analog to the traditional function pointer in the System.Delegate type. In C#, this type is mapped to the delegate keyword, so you’d declare and call a new delegate using code like this:
//declare a new delegate type
public delegate void SomeDelegate();
//create a new instance of SomeDelegate
SomeDelegate d = new SomeDelegate(ClassMethod);
//call the delegate function
VB.NET also maps the keyword Delegate to the System.Delegate type, although you’ll need to use the AddressOf keyword to assign an instance of a delegate, like this:
‘declare a new delegate type
Delegate Sub SomeDelegate()
‘create a new instance of SomeDelegate
Dim d as SomeDelegate
d = AddressOf ClassMethod
‘call the delegate function
The delegate solution
Check out Listing C for a solution using a delegate. Now, Notifier exposes a public delegate called NotifyDelegate, with return-type void and no parameters. Before Caller instantiates Notify, it creates an instance of NotifyDelegate, assigns Caller.Notify to that instance, and then passes it into Notifier’s constructor. When Notifer has finished its work in DoSomething, it invokes the delegate just as it would call any method, and Caller receives the notification it needs.
For the price of a few extra lines of code, you now have a very loosely coupled system. Any class can use Notifier as long as it implements a method with the appropriate signature and declares a delegate of the appropriate type. The delegated function can even be static/shared. If a class attempts to assign a function with a different method signature to NotifyDelegate, a compiler error results, so you’ll be able to catch the problem before the application is distributed.
Multicast vs. single-cast
The .NET Framework distinguishes between two kinds of delegates, those with multiple associated functions, referred to as multicast delegates, and those with only a single associated function, called single-cast delegates. With a multicast delegate, the .NET Framework internally builds a linked list of function pointers and calls each of them in turn when the delegate is invoked. Methods are added to the internal list via the Delegate class’s Combine method, which in C# is mapped to the += construct. To remove a method from a delegate’s invocation list, use the Remove method, or in C#, the -= operator.
Thus, to extend our original example a bit further, if a second class method, NotifyMeToo, also needs notification when Notifier finishes its work, you could use code like that found in Listing D. When NotifyDelegate is invoked, Caller.Notify and Caller.NotifyMeToo are each invoked in turn.
I should point out that the multiple methods in a delegate’s invocation list need not be on the same object instance or even declared as part of the same class. So in this case, NotifyMeToo doesn’t necessarily have to be a method on the same instance of Caller and could even be declared on a separate class from the original Notify method. Also, bear in mind this (hopefully) rather obvious point: Two different instances of Notify will have two different internal instances of NotifyDelegate. An invocation of one will not result in the calling of methods registered on the other.
Finally, a couple of rules govern what can and can't be a multicast delegate:
- · The return type of the delegate must be void (a sub in VB.NET).
- · The parameter list must not include any out parameters (byRef for VB.NET).
If these two rules are not met, any attempt to add a function to a delegate’s invocation list will result in a compile-time error.
You should now have a good working knowledge of .NET’s delegate system and be able to put them to some use in your applications. In future articles, we’ll explore delegates a bit further. If you’d like to suggest a specific topic, e-mail me. I’m all ears.