An useful property of delegate objects is that multiple objects can be assigned to one delegate instance using the + operator, process called delegate chaining.
Delegate chaining isn’t really useful until we will get to events and event subscribers, which will come in a future lesson, but it’s better to describe the behavior now, after you’ve seen a bit of delegates inner workings. So, let’s create a delegate, as we did in the previous lessons:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
using System; namespace HelloWorld { public class Program { delegate void SomeDelegate(); public static void Main() { SomeDelegate _delegate = Foo; _delegate(); _delegate = Goo; _delegate(); Console.Read(); } static void Foo() { Console.WriteLine("Foo()"); } static void Goo() { Console.WriteLine("Goo()"); } static void Sue() { Console.WriteLine("Soo()"); } } } |
I declared a delegate, SomeDelegate, and three methods that can be assigned to it, because they have the same signature: Foo(), Goo() and Sue(). Then, I created an instance of SomeDelegate, named _delegate, and I assigned Foo to it. Then, I invoked _delegate, I assigned Goo to it, and invoked it again, with this result:
Nothing new so far, but the way of doing it, assigning, and invoking, and then re-assigning and invoking again, it’s not very elegant. Besides, what if we want to call two methods at the same time when we invoke the delegate? As soon as we re-assign a new method to it, the original assignment gets lost.
The solution for these problems is delegate chaining. We can assign multiple methods to the same delegate, without overriding previous assigned methods, effectively creating a chain of methods assigned to it, which all get executed when we invoke the delegate:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
using System; namespace HelloWorld { public class Program { delegate void SomeDelegate(); public static void Main() { SomeDelegate _delegate = Foo; _delegate += Goo; _delegate += Sue; _delegate(); Console.Read(); } static void Foo() { Console.WriteLine("Foo()"); } static void Goo() { Console.WriteLine("Goo()"); } static void Sue() { Console.WriteLine("Soo()"); } } } |
It is really easy. I just used the += operator to add more methods to the existing assigned methods of my delegate. Of course, as explained in the lesson operators, x += y is just a shortened form of writing x = x + y. In other words, _delegate = _delegate + SomeMethod. And this is the result:
So, the important thing to get from this is that a delegate not necessarily references a single method. We can add multiple methods, we can remove from the added methods, and we can even add a method multiple times:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
using System; namespace HelloWorld { public class Program { delegate void SomeDelegate(); public static void Main() { SomeDelegate _delegate = Foo; _delegate += Goo; _delegate += Sue; _delegate += Foo; _delegate -= Foo; _delegate(); Console.Read(); } static void Foo() { Console.WriteLine("Foo()"); } static void Goo() { Console.WriteLine("Goo()"); } static void Sue() { Console.WriteLine("Soo()"); } } } |
If you execute the code, you will get this image:
so, the last assigned method with that name gets removed.
Let’s analyze further what happens behind the scenes when the compiler sees a delegate combine operation. We have this expression:
1 2 |
SomeDelegate _delegate = Foo; _delegate += Goo; |
which actually means this:
1 2 |
SomeDelegate _delegate = Foo; _delegate = _delegate + Goo; |
In this case, when the compiler encounters the + operator, it doesn’t actually perform addition between delegates, it combines them, which means that it references a static method called Delegate.Combine(), which takes a params Delegate array as argument, or, in our case, it takes _delegate as first parameter (the delegate to combine with) and Goo (the method to be combined with) as the second:
1 2 |
SomeDelegate _delegate = Foo; _delegate = Delegate.Combine(_delegate, Goo); |
At this point, you will get an error in Visual Studio, because the compiler doesn’t really know what to do with Goo: the compile time type is Delegate (because Delegate is the base class of all the delegate types, in an inheritance chain that follows this hierarchy: Delegate -> MulticastDelegate -> SomeDelegate), and we are giving it a method. We have to give it a delegate, so, it gets converted to this:
1 2 |
SomeDelegate _delegate = Foo; _delegate = Delegate.Combine(_delegate, new SomeDelegate(Goo)); |
In other words, it takes a new SomeDelegate and combines it with _delegate, which is also of type SomeDelegate. Again, you will still get an error, and this is because Delegate.Combine() returns a Delegate, and we are trying to assign it to a SomeDelegate variable. Hence, we need to do a casting:
1 2 |
SomeDelegate _delegate = Foo; _delegate = (SomeDelegate)Delegate.Combine(_delegate, new SomeDelegate(Goo)); |
As a conclusion, the compiler does quite a lot of code changes in order for us to just use += operator, instead of writing all that instruction.
You might ask yourself if you could directly assign more methods to a delegate, in a single assignment, like we can with primitive types. For instance:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
using System; namespace HelloWorld { public class Program { delegate void SomeDelegate(); public static void Main() { SomeDelegate _delegate = Foo + Goo + Sue; _delegate(); Console.Read(); } public static void Foo() { Console.WriteLine("Foo"); } public static void Goo() { Console.WriteLine("Goo"); } public static void Sue() { Console.WriteLine("Sue"); } } } |
In this case, you would actually get an error in Visual Studio, because the compiler thinks you want to add two methods together, which is impossible, not create a delegate chain assignment. If you want it to understand that we want a delegate chain, we can wrap the first method in a delegate:
1 |
SomeDelegate _delegate = new SomeDelegate(Foo) + Goo + Sue; |
In this case the compiler sees that we declare a new SomeDelegate, initialized with Foo, and it is this delegate that we are adding with Goo, which is allowed. The addition would also have a result of type SomeDelegate, which, when added to Sue, repeats the process. Finally, we can also cast the first operand:
1 |
SomeDelegate _delegate = (SomeDelegate)Foo + Goo + Sue; |
Finally, what would happen if one of the chained methods would throw an exception? How would the program cope with the remaining chained methods? Would they get executed? Lets see:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
using System; namespace HelloWorld { public class Program { delegate void SomeDelegate(); public static void Main() { SomeDelegate _delegate = (SomeDelegate)Foo + Goo + Sue; _delegate(); Console.Read(); } public static void Foo() { Console.WriteLine("Foo"); } public static void Goo() { throw new Exception(); } public static void Sue() { Console.WriteLine("Sue"); } } } |
In this case, I’ve modified Goo to throw an exception when executed. Let’s see the result when running the program:
So, whenever a method in the methods chain throws an exception, all the remaining methods will not get executed. In order to prevent this, I need to modify my program a bit, so I can handle exceptions with a Try…Catch block:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
using System; namespace HelloWorld { public class Program { delegate void SomeDelegate(); public static void Main() { SomeDelegate _delegate = (SomeDelegate)Foo + Goo + Sue; foreach (SomeDelegate item in _delegate.GetInvocationList()) { try { item(); } catch (Exception ex) { Console.WriteLine(ex.ToString()); } } Console.Read(); } public static void Foo() { Console.WriteLine("Foo"); } public static void Goo() { throw new Exception(); } public static void Sue() { Console.WriteLine("Sue"); } } } |
Delegate chains are immutable, just like strings, which means that you can create new ones as much as you want, but you cannot modify existing ones. This actually means that when you create a delegate and assign a method to it, the compiler creates an object in the Stack memory, which points to the address of that method, located on the Heap memory. If you add another method to the delegate, the compiler actually creates a new object on the Stack which points to the original method on the Heap, but it also points to a second delegate object, which in turn, points to the added method, while the original delegate object on the stack gets deleted by the Garbage Collector. So, the process of delegate chaining is quite complex under the hood.
Delegate chaining is usually used for a programming principle called the observer pattern, of which we will learn in the future.
Tags: delegate chaining, delegates, immutable, observer pattern
Thank you so much for this super clear explanation!
You’re welcome, kind stranger! 🙂