A delegate is an object that holds a reference to a method. A delegate can be used to implement a callback mechanism, for example to receive notification of an event such as a user clicking on a button. Delegates are also useful when we consider of the principle of separation of concerns. For example, we may want to keep business logic separate from generic algorithms. Delegates are an effective way of doing this.
Using delegates means that function calls are not bound to methods at compile time. Functionality can be ‘plugged in’ dynamically at runtime.
There are two aspects to implementing a delegate, the delegate type and the delegate instance.
The delegate type tells the compiler about the signature of the function we wish to call. A delegate instance is an object which specifies the target method – i.e. a concrete implementation conforming to the delegate type.
Let us consider the example of a callback for a button click event. The delegate type definition could be:
public delegate void ClickHandler(int buttonID)
In this case, a delegate instance is an object of type ClickHandler
. To create a delegate instance, we shall first define the target method, i.e. the method we wish to be called when the event occurs:
class Example
{
public OnClick(int id)
{
System.Console.WriteLine("You clicked!");
}
}
We can then define an instance of ClickHandler
as follows:
ClickHandler clickHandler = new ClickHandler(OnClick);
An alternative, shorter syntax is to use the method name directly in the assignment:
ClickHandler clickHandler = OnClick;
Once we have created the delegate, we can invoke it as follows:
clickHandler.Invoke(100)
where 100
is the button ID. This will then cause our OnClick()
method to be called. It is generally more common to use the following shorthand:
clickHandler(100)
Multicast delegates
It is also possible to chain delegate instances together. For example, if we had another click handler called OnClick2
whose signature matches the delegate type, we can ask for this method to be called also in the event of a button click. This is achieved using the +=
operator. For example, we could write:
ClickHandler clickHandler = new ClickHandler(OnClick);
clickHandler += new ClickHandler(OnClick2);
Now, when we invoke clickHandler
, both OnClick()
and OnClick2()
will be called. Methods will be called in the order that they were added to the delegate instance.
Delegate instances can be removed from the call chain using the -=
operator.
If the delegate type specifies a return type that is not void
, the return value from invoking the delegate is taken from the last method in the call chain.
Action and Func delegates
Action
and Func
are two generic delegates which can be used instead of user-defined delegates.
Action
is a general purpose delegate for methods which return void. There are several versions defined, corresponding to the number of arguments:
public delegate void Action();
public delegate void Action<in T>(T obj);
public delegate void Action<in T1,in T2>(T1 arg1, T2 arg2);
...
Func
is a generic delegate in which you can specify the return type as a template parameter.
Func
also has several versions, including the following:
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T,out TResult>(T arg);
public delegate TResult Func<in T1,in T2,out TResult>(T1 arg1, T2 arg2);
...
Events
Delegates are very handy for implementing the Observer design pattern. The event
keyword in C# refines the concept of delegates to fit more closely with this pattern.
As an example, let’s consider button class which allows subscribers to be notified when it is clicked.
The Button
class is defined as follows:
class Button
{
public delegate void ClickHandler(int buttonID);
public ClickHandler OnClick;
public void ButtonClick()
{
OnClick?.Invoke(99);
}
}
In summary, the delegate type is ClickHander
, the delegate instance is OnClick
. We also have a ButtonClick()
method which will invoke the OnClick
delegate so that any subscribers will be notified.
To use an event
instead of a delegate
, we would declare OnClick
as follows:
public event ClickHandler OnClick;
There are two important differences when using an event
instead of a delegate
:
- An
event
can only be invoked from the class in which it was defined. - Observers may only subscribe or unsubscribe to the event (i.e. using the
+=
and-=
operators). It’s not permitted assign to the event using the=
operator.
These two points are important in the context of the Observer pattern as they prevent different observers from interfering with each other.
Let’s complete the example by giving an example Dialog
class which subscribes to the OnClick
event:
class Dialog
{
public Button button = new Button();
public Dialog()
{
button.OnClick += new Button.ClickHandler(OnButtonClick);
}
public void OnButtonClick(int buttonID)
{
Console.WriteLine("Button clicked.");
}
}
The EventHandler delegate
C# comes with a built-in delegate called EventHandler
which is defined as follows:
public delegate void EventHandler(object sender, EventArgs e);
We can use the EventHandler
as a predefined delegate so that we can skip the definition of our custom delegate type.
This means we can replace the following two lines
public delegate void ClickHandler(int buttonID);
public ClickHandler OnClick;
with the single line:
public event EventHandler OnClick;
However, we must change our implementation of OnButtonClick()
to match the EventHandler
prototype.
public void OnButtonClick(object sender, EventArgs e)
{
...
}
The full working example is now as follows:
class Button
{
public event EventHandler OnClick;
public void ButtonClick()
{
OnClick?.Invoke(this, new EventArgs());
}
}
class Dialog
{
public Button button = new Button();
public Dialog()
{
button.OnClick += OnButtonClick;
}
public void OnButtonClick(object sender, EventArgs e)
{
Console.WriteLine("Button clicked.");
}
}
Note that the OnButtonClick
no longer receives the buttonID
. To fix this, we need to define our own custom event arguments.
Custom EventArgs
In addition to the built-in EventHandler
delegate, there is also a generic version, EventHandler<T>
which allows the user to specify the class to use for the event arguments. If no type is specified, the EventArgs
class is used as the default template parameter. So the following two lines are equivalent:
public event EventHandler OnClick;
public event EventHandler<EventArgs> OnClick;
To use a custom parameter list, we simply have to subclass the EventArgs
class.
A full implementation of our example is shown below:
class ButtonClickEventArgs
{
public int ButtonID { get; }
public ButtonClickEventArgs(int buttonID)
{
ButtonID = buttonID;
}
}
class Button
{
public event EventHandler<ButtonClickEventArgs> OnClick;
public void ButtonClick()
{
OnClick?.Invoke(this, new ButtonClickEventArgs(99));
}
}
class Dialog
{
public Button button = new Button();
public Dialog()
{
button.OnClick += OnButtonClick;
}
public void OnButtonClick(object sender, ButtonClickEventArgs e)
{
Console.WriteLine("Button clicked : {0}", e.ButtonID);
}
}
As we can see, the Dialog
class now receives the button ID in the event handler and displays it to the console.
Anonymous functions
We can use the delegate
keyword to define anonymous functions, i.e. methods which don’t have a name.
For example:
Func<int> func = delegate () { return 1; };
int val = func.Invoke();
Note that we are using the generic Func
delegate which comes predefined in C#. We could alternatively define our own delegate.
To handle the case where there is no return value, the Action
delegate can be used instead of Func
.