News & UpdatesProgrammingWeb programming StoreMy Projects
Links
Affiliates

C# Tutorial – 27 – Delegates

A delegate is a type used to reference a method. This allows methods to be assigned to variables and passed as arguments. The delegate’s declaration specifies the method signature to which objects of the delegate type can refer. Delegates are by convention named with each word initially capitalized followed by “Delegate” at the end of the name.

delegate void MyDelegate(string s);

A method that matches the delegate’s signature can be assigned to a delegate object of this type.

class MyApp
{
  static void Print(string t) { System.Console.Write(t); }
 
  static void Main()
  {
    MyDelegate d = Print;
  }
}

This delegate object will behave as if it was the method itself, no matter whether it refers to a static or an instance method. A method call on the object will be forwarded by the delegate to the method, and any return value will be passed back through the delegate.

MyDelegate d = Print;
d("Hello"); // "Hello"

The syntax used above to instantiate the delegate is actually a simplified notation that was introduced in C# 2.0. The backwards compatible way to instantiate a delegate is to use the regular reference type initialization syntax.

MyDelegate d = new MyDelegate(Print);

Anonymous methods

C# 2.0 also introduced anonymous methods, which can be assigned to delegate objects. An anonymous method is specified by using the delegate keyword followed by a method parameter list and body. This can simplify the delegate’s instantiation since a separate method will not have to be defined in order to instantiate the delegate.

MyDelegate f = delegate(string t) { System.Console.Write(t); };

Lambda expressions

C# 3.0 went one step further and introduced lambda expressions. They achieve the same goal as anonymous methods, but with a more concise syntax. A lambda expression is written as a parameter list followed by the lambda operator (=>) and an expression.

delegate int MyDelegate(int i);
static void Main()
{
  // Anonymous method
  MyDelegate a = delegate(int x) { return x * x; };
 
  // Lambda expression
  MyDelegate b = (int x) => x * x;
 
  a(5); // 25
  b(5); // 25
}

The lambda must match the signature of the delegate. Typically, the compiler can determine the data type of the parameters from the context, so they do not need to be specified. The parentheses may also be left out if the lambda has only one input parameter.

MyDelegate c = x => x * x;

If no input parameters are needed an empty set of parentheses must be specified.

delegate void MyEmptyDelegate();
// …
MyEmptyDelegate d = () => System.Console.Write("Hello");

A lambda expression that only executes a single statement is called an expression lambda. The expression of a lambda can also be enclosed in curly brackets to allow it to contain multiple statements. This form is called a statement lambda.

MyDelegate e = (int x) => { 
  int y = x * x;
  return y;
};

Multicast delegates

It is possible for a delegate object to refer to more than one method. Such an object is known as a multicast delegate and the methods it refers to are contained in a so called invocation list. To add another method to the delegate’s invocation list, either the addition operator or the addition assignment operator can be used.

static void Hi()  { System.Console.Write("Hi"); }
static void Bye() { System.Console.Write("Bye"); }
// …
MyDelegate d = Hi;
d = d + Hi;
d += Bye;

Similarly, to remove a method from the invocation list, the subtraction or subtraction assignment operators are used.

d -= Hi;

When calling a multicast delegate object, all methods in its invocation list will be invoked with the same arguments in the order that they were added to the list.

d(); // "HiBye"

If the delegate returns a value, only the value of the last invoked method will be returned. Likewise, if the delegate has an out parameter, its final value will be the value assigned by the last method.

Delegate signature

As mentioned before, a method can be assigned to a delegate object if it matches the delegate’s signature. However, a method does not have to match the signature exactly. A delegate object can also refer to a method that has a more derived return type than that defined in the delegate, or that has parameter types that are ancestors of the corresponding delegate’s parameter types.

class Base {}
class Derived : Base {}
 
delegate Base MyDelegate(Derived d);
 
class MyApp
{
  static Derived Dummy(Base o) { return new Derived(); }
 
  static void Main()
  {
    MyDelegate d = Dummy;
  }
}

Delegates as parameters

An important property of delegates is that they can be passed as method parameters. To demonstrate the benefit of this, two simple classes will be defined. The first one is a data storage class called PersonDB that has an array containing a couple of names. It also has a method that takes a delegate object as its argument, and calls that delegate for each name in the array.

delegate void ProcessPersonDelegate(string name);
 
class PersonDB
{
  string[] list = { "John", "Sam", "Dave" };
 
  public void Process(ProcessPersonDelegate f)
  { foreach (string s in list) f(s); }
}

The second class is Client, which will use the storage class. It has a Main method that creates an instance of PersonDB, and it calls that object’s Process method with a method that is defined in the Client class.

class Client
{
  static void Main()
  {
    PersonDB p = new PersonDB();
    p.Process(PrintName);
  }
 
  static void PrintName(string name)
  { 
    System.Console.WriteLine(name); 
  }
}

The benefit of this approach is that it allows the implementation of the data storage to be separated from the implementation of the data processing. The storage class only handles the storage and has no knowledge of the processing that is done on the data. This allows the storage class to be written in a more general way than if this class had to implement all of the potential processing operations that a client may want to perform on the data. With this solution, the client can simply plug its own processing code into the existing storage class.