Delegates in C#

1. Using Delegates

The specification of the delegate determines the form of the function, and to create an instance of the delegate, you must use a function that matches that form. Delegates are sometimes referred to as safe function pointers, which isn’t a bad analogy, but they do a lot more than act as function pointers.

Because of their dynamic nature, delegates are useful when the user may want to change behavior. If, for example, a collection class implements sorting, it might want to support different sort orders. You could control the sorting based on a delegate that defines the comparison function:

using System;

public class Container

{

public delegate int CompareItemsCallback(object obj1, object obj2);

public void Sort(CompareItemsCallback compare)

{

// not a real sort, just shows what the

// inner loop code might do

int x = 0;

int y = 1;

object iteml = arr[x];

object item2 = arr[y];

int order = compare(item1, item2);

}

object[] arr = new object[l]; // items in the collection

}

public class Employee {

Employee(string name, int id)

{

this.name = name;

this.id = id;

}

public static int CompareName(object obj1, object obj2)

{

Employee emp1 = (Employee) obj1;

Employee emp2 = (Employee) obj2;

return(String.Compare(emp1.name, emp2.name));

}

public static int CompareId(object obj1, object obj2)

{

Employee emp1 = (Employee) obj1;

Employee emp2 = (Employee) obj2;

if (emp1.id > emp2.id)

return(1);

if (emp1.id < emp2.id)

return(-1);

else

return(0);

}

string name;

int id;

}

class Test {

public static void Main()

{

Container employees = new Container();

// create and add some employees here

// create delegate to sort on names, and do the sort

Container.CompareItemsCallback sortByName =

new Container.CompareItemsCallback(Employee.CompareName);

employees.Sort(sortByName);

// employees is now sorted by name

}

}

The delegate defined in the Container class takes the two objects to be compared as parameters and returns an integer that specifies the ordering of the two objects. Two static functions are declared that match this delegate as part of the Employee class, with each function describing a different kind of ordering.

When the container needs to be sorted, you can pass in a delegate that describes the ordering that should be used, and the sort function will do the sorting. (Well, it would if it were actually implemented.)

2. Delegates to Instance Members

Users who are familiar with C++ will find a lot of similarity between delegates and C++ function pointers, but there’s more to a delegate than there is to a function pointer.

When dealing with Windows functions, it’s fairly common to pass in a function pointer that should be called when a specific event occurs. Since C++ function pointers can refer only to static functions and not member functions,[1] you need some way to communicate some state information to the function so it knows to which object the event corresponds. Most functions deal with this by taking a pointer, which is passed through to the callback function. The parameter (in C++ at least) is then cast to a class instance, and then the event is processed.

In C#, delegates can encapsulate both a function to call and an instance to call it on, so you don’t need an extra parameter to carry the instance information. This is also a type-safe mech­anism, because the instance is specified at the same time the function to call is specified, like so:

using System; public class User {

string name;

public User(string name)

{

this.name = name;

}

public void Process(string message)

{

Console.WriteLine(“{0}: {1}”, name, message);

}

}

class Test {

delegate void ProcessHandler(string message);

public static void Main()

{

User aUser = new User(“George”);

ProcessHandler ph = new ProcessHandler(aUser.Process);

ph(“Wake Up!”);

}

}

In this example, a delegate is created that points to the User.Process() function, with the aUser instance, and the call through the delegate is identical to calling aUser.Process() directly.

3. Multicasting

As mentioned earlier, a delegate can refer to more than one function. Basically, a delegate encapsulates a list of functions that should be called in order. The Delegate class provides functions; they take two delegates and return one that encapsulates both or that removes a delegate from another.

To combine two delegates, you can use the Delegate.Combine() function. For example, you can easily modify the previous example to call more than one function:

using System; public class User {

string name;

public User(string name)

{

this.name = name;

}

public void Process(string message)

{

Console.WriteLine(“{0}: {1}”, name, message);

}

}

class Test {

delegate void ProcessHandler(string message);

static public void Process(string message)

{

Console.WriteLine(“Test.Process(\”{0}\”)”, message);

}

public static void Main()

{

User user = new User(“George”);

ProcessHandler ph = new ProcessHandler(user.Process);

ph = (ProcessHandler) Delegate.Combine(ph, new ProcessHandler(Process));

ph(“Wake Up!”);

}

}

Invoking ph now calls both delegates.

This approach has a couple of problems, however. The first is that it’s not the best form. More important, however, is that it isn’t type-safe at compile time; Delegate.Combine() both takes and returns the type Delegate, so you have no way at compile time to know whether the delegates are compatible.

To address these issues, C# allows the += and -= operators to be used to call Delegate.Combine() and Delegate.Remove(), and it makes sure the types are compatible. You can modify the call in the example like so:

ph += new ProcessHandler(Process);

When invoked, the subdelegates encapsulated in a delegate are called synchronously in the order they were added to the delegate. If an exception is thrown by one of the subdelegates, the remaining subdelegates won’t be called. If this behavior isn’t desirable, you can obtain the list of subdelegates (otherwise known as an invocation list) from the delegate and call each subdelegate directly. Instead of this:

ph(“Wake Up!”);

you can use the following:

foreach (ProcessHandler phDel in ph.GetInvocationList())

{

try

{

phDel(“Wake Up!”);

}

catch (Exception e)

{

// log the exception here…

}

}

You can also use this code to implement “blackball” voting, where you can call all delegates once to see if they’re able to perform a function and then call them a second time if they all voted yes.

Wanting to call more than one function may seem to be a rare situation, but it’s common when dealing with events, which are covered in Chapter 24.

4. Delegates As Static Members

One drawback of this approach is that the user who wants to use the sorting has to create an instance of the delegate with the appropriate function. It’d be nicer if they didn’t have to do that. This can be avoided by defining the appropriate delegates as static members of Employee:

using System;

public class Container

{

public delegate int CompareItemsCallback(object obj1, object obj2); public void Sort(CompareItemsCallback compare)

{

// not a real sort, just shows what the

// inner loop code might do

int x = 0;

int y = 1;

object item1 = arr[x];

object item2 = arr[y];

int order = compare(item1, item2);

}

object[] arr = new object[1]; // items in the collection

}

class Employee

{

Employee(string name, int id)

{

this.name = name;

this.id = id;

}

public static readonly Container.CompareItemsCallback SortByName =

new Container.CompareItemsCallback(CompareName);

public static readonly Container.CompareItemsCallback SortById =

new Container.CompareItemsCallback(CompareId);

public static int CompareName(object obj1, object obj2)

{

Employee     emp1  =  (Employee)     obj1;

Employee     emp2  =  (Employee)     obj2;

return(String.Compare(emp1.name, emp2.name));

}

public static int CompareId(object obj1, object obj2)

{

Employee     emp1  =  (Employee)     obj1;

Employee     emp2  =  (Employee)     obj2;

if (empl.id > emp2.id)

return(l);

if (empl.id < emp2.id)

return(-l);

else

return(o);

}

string name; int id;

}

class Test {

public static void Main()

{

Container employees = new Container();

// create and add some employees here

employees.Sort(Employee.SortByName);

// employees is now sorted by name

}

}

This is a lot easier. The users of Employee don’t have to know how to create the delegate— they can just refer to the static member.

5. Delegates As Static Properties

One thing that’s a bit wasteful, however, is that the delegate is always created, even if it’s never used. It’d be better if the delegate were created on the fly as needed. You can do this by replacing the static members with properties:

using System;

class Container {

public delegate int CompareItemsCallback(object objl, object obj2); public void SortItems(CompareItemsCallback compare)

{

// not a real sort, just shows what the

// inner loop code might do…

int x = 0;

int y = 1;

object iteml = arr[x];

object item2 = arr[y];

int order = compare(iteml, item2);

}

object[] arr; // items in the collection

}

class Employee {

Employee(string name, int id)

{

this.name = name;

this.id = id;

}

public static Container.CompareItemsCallback SortByName {

get

{

return(new Container.CompareItemsCallback(CompareName));

}

}

public static Container.CompareItemsCallback SortById

{

get

{

return(new Container.CompareItemsCallback(CompareId));

}

}

static int CompareName(object obj1, object obj2)

{

Employee emp1 = (Employee) obj1;

Employee emp2 = (Employee) obj2;

return(String.Compare(emp1.name, emp2.name));

}

static int CompareId(object obj1, object obj2)

{

Employee emp1 = (Employee) obj1;

Employee emp2 = (Employee) obj2;

if (emp1.id > emp2.id)

return(1);

if (emp1.id < emp2.id)

return(-1);

else

return(0);

}

string name; int id;

}

class Test

{

public static void Main()

{

Container employees = new Container();

// create and add some employees here

employees.SortItems(Employee.SortByName);

// employees is now sorted by name

}

With this version, rather than Employee.SortByName being a delegate, it’s a function that returns a delegate that can sort by name.

Initially, this example had the private static delegate members SortByName and SortById, and the property created the static member if it hadn’t been used before. This works well if creating the delegate is somewhat costly and the delegate is likely to be used again.

In this case, however, it’s much easier to create the delegate on the fly and just return it to the user. As soon as the Sort function on Container is done with the delegate, it will be available for collection by the garbage collector.

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

Your email address will not be published. Required fields are marked *