Delegates and Events in C#
Article overview
In this article series, I’m going to review the basic features and techniques used behind the Language Integrated Query (LINQ), including :
- Delegates
- Events (which are not directly related to LINQ, but are very closely related to delegates)
- Lambda expressions (Anonymous functions)
- Extension methods
At the end, I’ll summarize them and, hopefully, you will possess a better understanding of how LINQ works. ;)
Related articles:
- LINQ internals (Part 2): Lambda expressions
- General types of programming paradigms (where you can read more about functional programming, one of the basic concepts related to the topic)
What is a delegate in C# ?
A delegate in C# is a class that holds pointers to one or more methods for later execution. It’s a wrapper that provides dynamic invocation on the stored method references, both synchronious and asynchronious, along with some related meta-information. In its simplest form, a delegate is just a pointer to a function.
Why were the delegates introduced ?
The delegates in C# were introduced in order to allow the developers to use techniques from the functional programming world in the procedural environment of the .Net platform.
For example, the delegates can be assigned to a variable and passed as function parameters, effectively making them “first class citizens”. That way, the inner workings of the methods can be replaced “dynamically” in a very elegant and effective way like, for instance, in LINQ, which is based on anonymous functions, extension methods and delegates.
template <class T> T Addition(T operand1, T operand2) { T result; result = operand1 + operand2; return result; }
Delegate basics
The delegate in its simplest form
As I mentioned, the delegate is just a class like the one below :
sealed class Operation: System.MulticastDelegate { public int Invoke(int x, int y); public IAsyncResult BeginInvoke(int x, int y, AsyncCallback cb, object state); public int EndInvoke(IAsyncResult result); }
This forms the very basic implementation of a delegate that derives from System.MulticastDelegate and System.Delegate, respectively. The method Invoke() calls the actual stored method, which in this case should be declared with two parameters of type integer. The other two methods – BeginInvoke() and EndInvoke() – are used in an asynchronous scenarios, when you need to execute the stored function in a different thread than the calling one.
The delegate shortcut
Luckily, we are not required to manually write the declaration and definition of the class shown above. The following declaration will automatically generate the code for us behind the scenes :
public delegate string MyDelegate(bool a, bool b, bool c);
For example :
namespace Test_ConsoleApp { public delegate int OperationDelegate(int param1, int param2); public class Operations { public int Addition(int op1, int op2) { return op1 + op2; } } public class Program { static void Main(string[] args) { Operations operationsInstance = new Operations(); OperationDelegate pointer = new OperationDelegate(operationsInstance.Addition); Console.WriteLine(operationsInstance.Addition(1, 2)); Console.WriteLine(pointer(1, 2)); Console.ReadLine(); } } }
If you explore the intellisense output on the delegate, you’ll notice that it contains a number of additional methods.
Using the method group conversion feature
Take a look on the following line :
OperationDelegate pointer = new OperationDelegate(operationsInstance.Addition);
Instead of instantiating the delegate class using the new keyword, we can use a feature called Method Group Conversion to directly assign the method to the delegate pointer. For instance :
OperationDelegate pointer = operationsInstance.Addition;
Using the multicast feature
Remember when I said that delegates store pointers to one or more methods ? Storing and calling multiple methods is called multicasting and can be achieved the following way :
OperationDelegate pointer = new OperationDelegate(operationsInstance.Addition); pointer += operationsInstance.Addition; pointer += operationsInstance.Subsctraction; pointer += operationsInstance.SomeOtherCoolOperation; Console.WriteLine(pointer(1, 2));
When you invoke the delegate implicitly or explicitly, all the stored methods will get invoked. The snippet above uses operator overloading, which is a really cool feature that originates from C++.
Using the predefined delegates
The truth is that you rarely want or need to define any delegates yourself. There is a group of predefined generic delegates which you can use when you need a rather standard implementation :
public delegate TResult Func<TResult>(); public delegate TResult Func<T1, TResult>(T1 arg1); public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2); public delegate TResult Func<T1, T2, T3, TResult>(T1 arg1, T2 arg2, T3 arg3); public delegate TResult Func<T1, T2, T3, T4, TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4); public delegate void Action(); public delegate void Action<T1>(T1 arg1); public delegate void Action<T1, T2>(T1 arg1, T2 arg2); public delegate void Action<T1, T2, T3>(T1 arg1, T2 arg2, T3 arg3); public delegate void Action<T1, T2, T3, T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4); public delegate int Comparison<T>(T x, T y); public delegate TOutput Converter<TInput, TOutput>(TInput input); public delegate bool Predicate<T>(T obj); public delegate void EventHandler<TEventArgs>(Object sender, TEventArgs e) where TEventArgs : EventArgs;
Some of them have similar declarations, but they are semantically different. Namely, if you want to point to a predicate, you’ll use the Predicate<T> class, even if there is a Func<T1, Result> that takes practically the same arguments.
Events basics
What are the events in C Sharp ?
The events in C# are in fact simple wrappers around the delegates, which provide few convenient features that are really just a syntactic sugar. Consider the following example :
namespace Test_ConsoleApp { public delegate void OperationInvoked(string message); public class Operations { private OperationInvoked callback = null; public void RegisterEvent(OperationInvoked function) { this.callback += function; } public void UnRegisterEvent(OperationInvoked function) { this.callback += function; } public int Addition(int op1, int op2) { this.callback("Addition performed."); return op1 + op2; } public int Subsctraction(int op1, int op2) { this.callback("Substraction performed."); return op1 - op2; } } public class Program { public static void Main(string[] args) { Operations operationsInstance = new Operations(); operationsInstance.RegisterEvent(Program.PrintMessage); operationsInstance.Addition(1, 2); operationsInstance.Subsctraction(1, 2); Console.ReadLine(); } public static void PrintMessage(string message) { Console.WriteLine(message); } } }
We have the Operations class that performs some business logic and that we want to observe (which is basically an implementation of the Observer pattern using the latest .Net features). We see the delegate member, which is declared as private and, we see a pair of accessors – a getter and a setter.
The whole idea is, of course, to provide an appropriate encapsulation. We don’t want to give exclusive access to our delegate, we don’t want anyone to be able to override the delegate’s already established list of methods. That’s why we declare it as private, and that’s why we provide accessor methods
So, what are the events after all ?
The events in C Sharp generate some of the code for you and provide few more minor conveniences. The following declaration
public event OperationInvoked operationPerformedEvent;
will actually generate
- A private declaration of a delegate of the specified class
- Accessors (a getter and a settor), which prohibits the user from directly accessing the delegate and it’s methods
In addition to that, the events provide the following conveniences :
- An event can be used in an interface declaration, while a delegate can not
- An event can only be invoked from the class in which it is declared
- The event has to be declared in a specific way
Using events, our example becomes the following :
namespace Test_ConsoleApp { public delegate void OperationInvoked(string message); public class Operations { public event OperationInvoked callback; public int Addition(int op1, int op2) { this.callback("Addition performed."); return op1 + op2; } public int Subsctraction(int op1, int op2) { this.callback("Substraction performed."); return op1 - op2; } } public class Program { public static void Main(string[] args) { Operations operationsInstance = new Operations(); operationsInstance.callback += Program.PrintMessage; operationsInstance.Addition(1, 2); operationsInstance.Subsctraction(1, 2); Console.ReadLine(); } public static void PrintMessage(string message) { Console.WriteLine(message); } } }
Notice that if we attempt to assign a new value to the callback
operationsInstance.callback = Program.PrintMessage;
a compile-time error will be thrown. In fact, the IDE itself will warn us before that.
Using the proper convention for declaring events
As I said in the previous section, the event has to be declared in a specific way. Or at least it’s a good practice to define in that way, using the following pattern :
public delegate void OperationInvoked(object sender, OperationArgs e);
You see the first parameter which is of type System.Object, and the second parameter, which is in fact a custom type that derives from System.EventArgs
public class OperationEventArgs : EventArgs { public readonly string _message; public OperationEventArgs(string message) { _message = message; } }
If you take a look on all the standard Microsoft event implementations, you’ll notice exactly this pattern. It decouples the event declaration from the actual parameters passed, making them more orthogonal.
Conclusion
Pointing to methods, using them as variables and passing them through functions is in fact a very powerful technique. Although I would not use a pure functional programming language for everyday use, I do admire the benefits that come from this hybrid approach.
In this article, I’ve made a brief overview of the delegates in C#, which are critical for understanding the inner workings of LINQ. In my next articles on the topic, I’ll talk about Anonymous functions and Extension methods.
LINQ internals (Part 2): Lambda expressions