Delegates 2020
Conceptually, a delegate is nothing more than a type-safe method reference.
A delegate allows us to delegate the act of calling a method to somebody else.
A delegate also can be thought of as an object that contains an ordered list of methods with the same signature and return type.
- The list of methods is called the invocation list.
- When a delegate is invoked, it calls each method in its invocation list.
A delegate dynamically wires up a method called to its target method. There are two aspects to a delegate:
- Type
A delegate type defines a protocol to which the called and target will conform, compromising a list of parameter types and a return type.delegate void MyDelegate (index x);
Here, MyDelegate is the delegate type name. - Instance
A delegate instance refers to one or more target methods conforming to that protocol.
Let's take a step back and gain a little bit of perspective. The Windows API is using C-style function pointers to create callback functions. Using callbacks, programmers could configure one function to report back to (call back) another function. By doing this, Win32 developers could handle button click, mouse move, and so on. This way they could setup bidirectional communications between two functions.
The problem with standard C-style callback functions is that they represent little more than a raw address in memory. Callbacks could have been configured to include additional type-safe information such as the number of parameters and the return value of the method pointed to. Because of these kinds of lacking features have caused numerous bugs, hard crashes, and other runtime problems.
Callbacks are still possible, and their functionality is accomplished in much safer and more object-oriented manner using delegates. Actually, a delegate is a type-safe object that points to another method which can be invoked at a later time. A delegate object maintains three important pieces of information:
- The address of the method on which it makes calls
- The argument of this method.
- The return value of this method
The delegate can be used to call any method with a matching signature. The name of the delegate, the names of the target methods, and the names of those methods' parameter are of no importance. The only requirement is that the methods being called have the exactly matching signature.
A delegate instance literally acts as a delegate for the caller: the caller invokes the delegate, and then the delegate calls the target method. This indirection decouples the caller from the target method.
A delegate type declaration is preceded by the keyword delegate, but otherwise it resembles an abstract method declaration. We must define the delegate to match the signature of the method it will point to.
For instance, if we want to build a delegate named BinaryOp that can point to any method that returns an integer and takes two integers as parameters:
public delegate int BinaryOp(int x, int y);
When the compiler processes delegate type, it generates a sealed class deriving from System.MulticastDelegate. This class in conjunction with its base class, System.Delegate provides the necessary infrastructure for the delegate to hold onto a list of methods to be involved at a later time.
Let's take a deep look at the inside of delegate. If we examine the BinaryOp using ildasm.exe after opening executable from the following code:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace DelegateTest { class Program { public delegate int BinaryOp(int x, int y); static void Main(string[] args) { } } }
we get the assembly showing a delegate which represents a sealed class deriving from System.MulticastDelegate:
As we can see, the compiler-generate BinaryOp class defines three public methods: Invoke(), BeginInvoke(), and EndInvoke().
Invoke() is used to invoke each method maintained by the delegate type is called behind the scenes when we use the appropriate C# syntax. Invoke() is synchronous. BeginInvoke(), and EndInvoke() provide the ability to call the current method asynchronously on a separate thread of execution.
How exactly does the compiler know how to define the Invoke(), BeginInvoke(), and EndInvoke() methods? To understand the process, let's look at the compiler-generated BinaryOp class:
sealed class BinaryOp: System.MulticastDelegate { public BinaryOp(object target, unit functionAddress); public int invoke(int x, int y, AsyncCallback cb, object state); public int EndInvoke(IAsyncResult result); }
Note that the parameters and return value define for the Invoke() method exactly match the definition of the BinaryOp delegate. The initial parameters to BeginInvoke() members are also based on the BinaryOp delegate. But BeginInvoke() will always provide two final parameters which are AsyncCallback and object. They are used to facilitate asynchronous method invocations. The return value of EndInvoke() is identical to the original delegate declaration and will always take as a sole parameter an object implementing the IAsyncResult interface.
When we build a type using delegate keyword, we indirectly declare a class type that derives from System.MulticastDelegate. This class provides descendents with access to a list that contains the addresses of the methods maintained by the delegate type as well as several additional methods to interact with the invocation list.
A delegate type declaration is preceded by the keyword delegate:
delegate int Morph( int x);
To create a delegate instance, we can assign a method to a delegate variable:
using System; namespace DelegateTestA { class Program { delegate int Morph(int x); static void Main(string[] args) { // create delegate instance Morph m = Square; // invoke delegate int result = m(3); Console.WriteLine(result); } static int Square(int x) { return x * x; } } }
Invoking a delegate is just like invoking a method since the delegate's purpose is merely to provide a level of indirection:
m(3);
The statement:
Morph m = Square;
is shorthand for:
Morph m = new Morph(Square);
using System; namespace SimpleDelegate { // This delegate can point to any method // which takes two ints and returning an int public delegate int Simple(int x, int y); // This class contains methods Simple will point to public class MyMath { public static int add(int x, int y) { return x + y; } public static int mult(int x, int y) { return x * y; } public static int square(int x) { return x * x; } } class Program { static void Main(string[] args) { // create a Simple object // that points to Simple(MyMath.add()) Simple s = new Simple(MyMath.add); // invoke add() method indirectly using delegate object Console.WriteLine("4 + 10 = {0}", s(4,10)); // explicitly calling Invoke() Console.WriteLine("4 + 10 = {0}", s.Invoke(4, 10)); // This is an error // because there is a type mismatch // Simple s2 = new Simple(MyMath.square); Console.ReadLine(); } } }
Output is:
4 + 10 = 14
Note that the Simple delegate can point to any method taking two integers and returning an integer and the actual name of the method pointed to irrelevant. Here, we created a class named MyMath, which defines two static methods that match the pattern defined by the Simple delegate.
When we want to insert the target method to a given delegate, simply pass in the name of the method to the delegate's constructor:
Simple(MyMath.add);
At this point, we can invoke the member pointed to by using a syntax that looks like a direct function invocation:
Console.WriteLine("4 + 10 = {0}", s(4,10));
Under the hood, the runtime actually calls the compiler-generated Invoke method. We can verify this fact by checking assembly in ildasm.exe and investigate the CIL code within the Main() method.
We can explicitly call the Invoke() from our code directly:
Console.WriteLine("4 + 10 = {0}", s.Invoke(4, 10));
Since .NET delegates are type safe, if we attempt to pass a delegate a method that does not match the delegate pattern, we get a compile-time error:
Simple s2 = new Simple(MyMath.square);
Let's make a method that will print out names of the methods maintained by the incoming delegate type as well as the name of the class defining the method. We'll iterate over the System.Delegate array returned by GetInvocationList(), invoking each object's Target and Method properties:
using System; namespace DelegateTestA { class Program { delegate int Morph(int x); static int Square(int x) { return x * x; } static void DelegateInfo(Delegate dObj) { foreach (Delegate d in dObj.GetInvocationList()) { Console.WriteLine("Method: {0}", d.Method); Console.WriteLine("Type: {0}", d.Target); } } static void Main(string[] args) { // create delegate instance Morph m = Square; DelegateInfo(m); // invoke delegate Console.WriteLine("result = {0}", m(3)); } } }
Output is:
Method: Int32 Square(Int32) Type: result = 9
All delegate instances have the intrinsic ability to multicast. In other words, a delegate object can maintain a list of methods to call, rather than a single method. When we want to add multiple methods to a delegate object, we simply use overloaded += operator, rather than a direct assignment.
For example:
SomeDelegate d = MethodA; d += MethodB;
Invoking d will now call both MethodA and MethodB. Delegates are invoked in the order they are added.
The -= method removes the right delegate operand from the left delegate operand. For example:
d -= MethodA;Invoking d will now cause only MethodB to be invoked. Calling += on a delegate variable with a null value works, and it is equivalent to assigning the variable to a new value:
SomeDelegate d = null; d += MethodA
If a multicast delegate has a non-void return type, the called receives the return value from the last method to be invoked. The preceding methods are sill called, but their return value are discarded. In most cases in which multicast delegates are used, they have void return types, so this subtlety does not arise.
All delegate types implicitly inherit System.MulticastDelegate, which inherits from System.Delegate. C# compilers += and -= operations made on a delegate to the static Combine() and Remove() methods of thee System.Delegate class.
Here is the example code:
using System; namespace DelegateTest { public delegate void ProgressReporter(int percentComplete); public class Util { public static void HardWork(ProgressReporter p) { for (int i = 0; i < 10; i++) { p(i * 10); System.Threading.Thread.Sleep(100); } } } class Program { static void Main(string[] args) { // create delegate instance ProgressReporter p = WriteProgressToConsole; p += WriteProgressToFile; Util.HardWork(p); } static void WriteProgressToConsole(int percent) { Console.WriteLine(percent); } static void WriteProgressToFile(int percent) { System.IO.File.WriteAllText("progress.txt", percent.ToString()); } } }
Output to the console is:
0 10 20 30 40 50 60 70 80 90
When a delegate instance is assigned to an instance method, the delegate instance must maintain a reference not only to the method, but also to the instance of that method. The System.Delegate class's Target property represent this instance and will be null for a delegate referencing a static method. For example:
using System; namespace DelegateTest { public delegate void ProgressReporter(int percentComplete); class Program { static void Main(string[] args) { new Program(); } Program() { ProgressReporter p = InstanceProgress; p(99); Console.WriteLine(p.Target == this); Console.WriteLine(p.Method); } void InstanceProgress(int percent) { Console.WriteLine(percent); } } }
Output is:
99 True Void InstanceProgress(Int32)
A delegate type may contain generic type parameters. For instance:
public delegate T MorphWith this definition, we can write a generalized Morph utility method that works on any type:
using System; namespace DelegateTest { public delegate T Morph(T arg); public class Util { public static void Morph (T[] values, Morph m) { for (int i = 0; i < values.Length; i++) values[i] = m(values[i]); } } class Program { static void Main(string[] args) { int[] values = new int[] { 10, 20, 30 }; Util.Morph(values, Square); foreach (int i in values) Console.Write("{0} ", i); } static int Square(int x) { return x * x; } } }
Output is:
100 400 900
A problem that can be solved with a delegate can also be solved with an interface. For example, the following explains how to solve our filter problem using an IMorph interface:
using System; namespace DelegateTest { public interface IMorph { int Morph(int x); } public class Util { public static void MorphAll(int[] values, IMorph m) { for (int i = 0; i < values.Length; i++) values[i] = m.Morph(values[i]); } } class Program : IMorph { static void Main(string[] args) { int[] values = new int[] { 10, 20, 30 }; Util.MorphAll(values, new Program()); foreach (int i in values) Console.Write("{0} ", i); } public int Morph(int x) { return x * x; } } }
A delegate design may be a better choice than an interface design if one or more of these conditions are true:
- The interface defines only a single method.
- Multicast capability is needed.
- The listener needs to implement the interface multiple times.
In the IMorph example, we don't need to multicast. However, the interface defines only a single method. Furthermore, our listener may need to implement IMorph multiple times, to support different morphs like cure. With interfaces, we're forced into writing a separate type per morph, since Program can only implement IMorph once. This is quite cumbersome:
using System; namespace DelegateTest { public interface IMorph { int Morph(int x); } public class Util { public static void MorphAll(int[] values, IMorph m) { for (int i = 0; i < values.Length; i++) values[i] = m.Morph(values[i]); } } class Program { static void Main(string[] args) { int[] values = new int[] { 10, 20, 30 }; Util.MorphAll(values, new Cube()); foreach (int i in values) Console.Write("{0} ", i); } class Square: IMorph { public int Morph (int x) { return x * x; } } class Cube: IMorph { public int Morph(int x) { return x * x * x; } } } }
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization