Delegates are used to encapsulate functions into callable function objects that are used much as function pointers are used in C, C++, and other languages. Like function pointers, delegates enable you to separate a function reference from its implementation, allowing the implementation to exist in a separate module. Unlike function pointers, delegates are type-safe, enabling the compiler and the Microsoft .NET Framework to ensure that code called via a delegate is executed safely.
The most common use of delegates is in those cases in which a traditional C or C+ programmer would use a function pointer. For example, in Chapter 10, we’ll use delegates to provide thread handling methods in multithreaded applications. A thread handling function is created as a delegate, which is called in a separate thread by the runtime, as shown in Figure 6-1.
Delegates are widely used in C# to provide event handlers when you’re developing Windows Forms applications or when you’re using Web Forms with Microsoft ASP.NET. Multiple delegates can be automatically chained together, as shown in Figure 6-2, making it easy to build applications that use layers of delegates to handle events. (Using delegates for event handling is discussed later in this chapter, in the section “Handling Events.”)
Delegates in C# are declared in a two-step process. First a delegate type is declared, and then instances of the delegate type are created, as shown in the following code. A delegate type is declared using the delegate keyword along with the method signature of the delegate.
public delegate void OpDelegate(int First, int Second);
Any method that conforms to the declared signature can be used as a delegate. Unlike function pointers in C++, any static or non-static member method can be used for your delegate in C#, as long as the signature matches. Here are some examples of methods that can be used with the preceding delegate declaration:
static public void Subtract(int First, int Second) { } public void Add(int First, int Second) { }
To create a delegate, use the new keyword to create a new instance of the delegate type, passing the method name as an argument. In the following code, the method name passed as a parameter serves as the delegate’s target method. When the delegate is invoked, the target method will be called.
new MathOp.OpDelegate(AddFunc.Add);
When a delegate is created, the C# compiler actually generates a great deal of code behind the scenes for you. All delegates written in C# are actually types derived from the System.MulticastDelegate class. When you create a new instance of a delegate, you’re creating new objects that are subclassed from MulticastDelegate.
The System.MulticastDelegate class is derived from System.Delegate, a more limited delegate type. The MulticastDelegate class extends Delegate, adding methods and internal data structures that allow delegates to be chained together. When a delegate type is created using the C# delegate keyword, it’s always a subclass of MulticastDelegate rather than Delegate.
Although the simplest way to declare a delegate is to use the C# delegate keyword, you also can create classes that derive from System.MulticastDelegate or System.Delegate directly. In most cases, however, you’ll get no benefit from using a handcrafted delegate class, especially because you can use the delegate keyword and still gain access to the methods declared for the MulticastDelegate class. Later in this chapter, in the section “Combining Delegates,” we’ll use methods from MulticastDelegate to manage types created using the delegate keyword.
The most common use of a delegate is to call back to a method that will perform a required task. A delegate used to perform callback operations is typically passed as a parameter to a constructor or another method and invoked when the callback is executed. The following example uses a delegate when performing a bubble sort on an array:
public class BubbleSort { /// <summary> /// A delegate is used to define the sort order for two /// elements in the table. /// </summary> public delegate bool Order(object first, object second); /// <summary> /// Sort elements in an Array object, using the Order /// delegate to determine how items should be sorted. /// </summary> /// <param name="table">Array to be sorted</param> /// <param name="sortHandler">Delegate to manage /// sort order.</param> public void Sort(Array table, Order sortHandler) { if(sortHandler == null) throw new ArgumentNullException(); bool nothingSwapped = false; int pass = 1; while(nothingSwapped == false) { nothingSwapped = true; for(int index = 0; index < table.Length - pass; ++index) { // Use an Order delegate to determine the sort order. if(sortHandler(table.GetValue(index), table.GetValue(index + 1)) == false) { nothingSwapped = false; object temp = table.GetValue(index); table.SetValue(table.GetValue(index + 1), index); table.SetValue(temp, index + 1); } } ++pass; } } }
The BubbleSort class defines a delegate type named Order that’s used to determine the relative sort order for elements in an array. Methods that are associated with the Order delegate type accept the two objects that are compared to determine their relative sort order as parameters. The method is expected to return true if the first item should be sorted into a lower position than the second item and return false otherwise.
The BubbleSort.Sort method accepts two parameters: an array to be sorted, and an instance of the Order delegate that will be invoked to perform the sort. The Sort method uses the bubble sort algorithm to iterate over the array to be sorted. Part of the bubble sort algorithm requires that adjacent items in the array be compared to determine whether they’re in the correct order. If they’re not, the items are swapped. In the Sort method, the comparison is done using the delegate passed as a parameter. Enabling a client of the Sort method to define a sort order delegate allows the Sort method to be used with any type of object, because only the delegate needs to understand how array elements are sorted.
An application that uses the BubbleSort class to sort an array of integers is shown here:
class DelegateSortApp { static int [] sortTable = {5,4,6,2,8,9,1,3,7,0}; static void Main(string[] args) { DelegateSortApp app = new DelegateSortApp(); // Print array in original order. foreach(int i in sortTable) Console.WriteLine(i); Console.WriteLine("Sorting"); BubbleSort sorter = new BubbleSort(); BubbleSort.Order order = new BubbleSort.Order(SortOrder); sorter.Sort(sortTable, order); foreach(int i in sortTable) Console.WriteLine(i); Console.WriteLine("Done"); } // Delegate method; returns true if first is less than second static public bool SortOrder(object first, object second) { int firstInt = (int)first; int secondInt = (int)second; return firstInt < secondInt; } }
The DelegateSortApp class shown here defines the type of elements stored in the array as well as the method used to compare elements in the array. In this example, the array contains int values, but it could easily contain any other type. The DelegateSortApp.SortOrder method is used for comparing array elements. This method matches the signature of the BubbleSort.Order delegate, which is invoked by the BubbleSort class to determine the relative sort order of array elements. The algorithms used to sort the array are encapsulated in the BubbleSort class, whereas the array definition as well as the knowledge required to manage the array elements are kept in the DelegateSortApp class.
Another way to use delegates is as functors, or classes that implement methods that can be “plugged in” to provide functionality for other classes. For example, consider a delegate declaration that defines a math operation:
public delegate void OpDelegate(int First, int Second);
The AddFunc class in the following code implements a single method, Add, which conforms to the signature of OpDelegate:
public class AddFunc { static public void Add(int First, int Second) { int res = First + Second; Console.WriteLine("{0}+{1}={2}", First, Second, res); } }
The following class, MathOp, exposes a public field named Op, which is an instance of OpDelegate. When MathOp.Invoke is called by a client, the Op delegate is called to perform a math operation on operands stored in the MathOp object.
public class MathOp { public delegate void OpDelegate(int First, int Second); public OpDelegate Op; public MathOp(int First, int Second) { _first = First; _second = Second; } public void Invoke() { if(Op != null) Op(_first, _second); } protected int _first; protected int _second; }
This example code illustrates how the MathOp class is used:
class MathDelegateApp { static void Main(string[] args) { MathOp mo = new MathOp(42, 27); mo.Op = new MathOp.OpDelegate(AddFunc.Add); mo.Invoke(); } }
An instance of the MathOp class is created by passing two operands as parameters to the constructor. An instance of OpDelegate is created and assigned to the public field Op. Finally the MathOp.Invoke method is called, which invokes the delegate and calls AddFunc.Add. The MathOp class is completely generic and doesn’t track the details of the math operations performed. The implementation of math operations is encapsulated in OpDelegate objects, which can be plugged in to the MathOp class as needed.
As mentioned, delegates can be linked together easily, enabling you to combine multiple delegate objects into a chain of delegates. This capability is built into the .NET Framework’s System.MulticastDelegate class. Each delegate can track the delegates located ahead of and behind it in the delegate chain. Invoking a method on a delegate implicitly causes each delegate in the chain to be invoked sequentially.
The System.MulticastDelegate class includes member functions that are used to chain delegates together. The Delegate class also overrides a number of operators that simplify the task of managing chains of delegates. To combine two or more delegates, you can use the same addition operators (+ and +=) that you would use on primitive types, as shown here:
mo.Op = new MathOp.OpDelegate(AddFunc.Add) + new MathOp.OpDelegate(SubtractFunc.Subtract); mo.Op += new MathOp.OpDelegate(MultFunc.Multiply);
Delegates are removed from a delegate chain using the subtraction operators (- and -=), as follows:
mo.Op = mo.Op - new MathOp.OpDelegate(MultFunc.Multiply); mo.Op -= new MathOp.OpDelegate(MultFunc.Multiply);
You must have a reference to the delegate to remove it. If you plan to remove a delegate, you can keep a reference to the delegate when it’s originally created, or you can create a new instance of the delegate specifically for the removal operation.
An example of using a chain of delegates is shown in the following code. The MultFunc and SubtractFunc classes implement methods that can be used with the MathOp class (and its OpDelegate member) shown in the example in the preceding section, “Using Delegates as Functors.”
public class MultFunc { static public void Multiply(int First, int Second) { int res = First * Second; Console.WriteLine("{0}*{1}={2}", First, Second, res); } } public class SubtractFunc { static public void Subtract(int First, int Second) { int res = First - Second; Console.WriteLine("{0}-{1}={2}", First, Second, res); } }
The enhanced version of the MathDelegateApp class shown here creates three delegate objects, with each delegate targeting one of the AddFunc.Add, SubtractFunc.Subtract, and MultFunc.Multiply methods:
class MathDelegateApp { static void Main(string[] args) { MathOp mo = new MathOp(42, 27); mo.Op += new MathOp.OpDelegate(AddFunc.Add); mo.Op += new MathOp.OpDelegate(SubtractFunc.Subtract); mo.Op += new MathOp.OpDelegate(MultFunc.Multiply); mo.Invoke(); } }
In this example, when mo.Invoke is called, each of the delegates will be called in turn. The output from this version of the MathDelegate project is shown here:
42+27=69 42-27=15 42*27=1134
Remember, inside the MathOp.Invoke method, the Op delegate is called directly, without any iteration statements, as follows:
public void Invoke() { if(Op != null) Op(_first, _second); }
When Op is called, the delegate automatically takes care of iterating over the list of delegates, calling each in turn. This behavior raises an issue when you’re dealing with multiple return values or when exceptions are thrown by a delegate. When multiple delegates are invoked, any exceptions thrown by a delegate that aren’t handled in the delegate are first passed to the method that invoked the delegate, just as if the method had invoked the delegate directly. Because the exception transfers control back to the method that invoked the delegate, no delegates farther down in the invocation list will be executed.
If you’re using delegates that must throw exceptions, you can take advantage of a delegate invocation technique that manually iterates over the delegate list and invokes each delegate separately. This is a useful way to execute your delegates if you need fine-grained control over exception handling or managing return values. First let’s look at a simple version of MathOp.Invoke, rewritten with manual iteration over the delegate chain:
public void Invoke() { object[] args = new object[2]; args[0] = _first; args[1] = _second; Delegate [] operators = Op.GetInvocationList(); foreach(Delegate d in operators) { d.DynamicInvoke(args); } }
This example retrieves a list of the delegate instances by calling the delegate’s GetInvocationList method. GetInvocationList returns an array of System.Delegate objects that make up the delegate chain. The .NET Framework Delegate class is used in this context because, as shown in the following code, you can’t use the C# delegate keyword to declare an object, only a type:
// Error; won't compile. Must use Delegate class. delegate [] operators = Op.GetInvocationList();
The foreach statement is then used to iterate over the invocation list. Because delegates can have any number of parameters, delegate parameters are passed as an object array to the DynamicInvoke method. The Delegate class then manually binds to the method associated with the delegate, throwing a runtime exception if the parameters don’t match the called method.
The following version of MathOp.Invoke expands on the preceding example by handling any exceptions thrown by delegate methods. When an exception is thrown, the exception is logged, and a recovery action is taken before the next delegate in the list is invoked.
public void Invoke() { object[] args = new object[2]; args[0] = _first; args[1] = _second; Delegate [] operators = Op.GetInvocationList(); foreach(Delegate d in operators) { try { d.DynamicInvoke(args); } catch(Exception exc) { LogException(exc); Recover(exc); } } }
Exception handling is just one reason to explicitly manage the delegate invocation list. Other scenarios include explicit management of reference parameters or return values.
By default, reference parameters are passed to each delegate method in turn. If a method changes the state of a reference parameter, delegates farther down in the invocation list will receive the changed parameter. Explicit management of delegate invocation allows you to alter this behavior.
The return value for a delegate chain is the return value of the last delegate method. The return values from methods called earlier in the delegate chain are simply discarded. If you explicitly manage the invocation of delegates, you have the opportunity to examine the return value for each method in the delegate chain.
The delegate examples presented up to now have been associated with static methods. It’s often useful to associate a delegate with a non-static method, providing the method with easy access to fields defined for a class. When creating a new delegate that’s associated with a non-static method, you must provide a specific instance of the class when passing the method to the delegate constructor, as shown here:
delegate bool ScoringPolicy(int questionCount, int correct); class TestScore { public TestScore(decimal minimumScore) { _minimum = minimumScore; } public bool PassOrFail(int questions, int correct) { decimal score = (decimal)correct/(decimal)questions; return _minimum <= score; } protected decimal _minimum; } ScoringPolicy midTermPolicy = null; ScoringPolicy finalPolicy = null; // Create a delegate that uses a minimum score of 70. TestScore midTermExam = new TestScore(70); midTermPolicy = new ScoringPolicy(midTermExam.PassOrFail); // Create a delegate that uses a minimum score of 75. TestScore finalExam = new TestScore(75); finalPolicy = new ScoringPolicy(finalExam.PassOrFail);
The following code presents a more complete example of using a delegate with non-static methods. The Account class models a bank account that provides overdraft protection for individual accounts. The class includes methods for handling deposits and withdrawals, as well as tracking the current account balance.
public class Account { public delegate bool DebitPolicy(decimal aWithdrawal); public Account(decimal anInitialBalance, DebitPolicy aDebitPolicy) { _balance = anInitialBalance; _debitPolicy = aDebitPolicy; } public decimal Balance { get { return _balance; } } public void Deposit(decimal aDeposit) { if(aDeposit < 0) throw new ArgumentOutOfRangeException(); _balance += aDeposit; } public bool Withdrawal(decimal aWithdrawal) { if(aWithdrawal < 0) throw new ArgumentOutOfRangeException(); if(aWithdrawal < _balance) { _balance -= aWithdrawal; return true; } else { // If no debit policy, no overdrafts are permitted. if(_debitPolicy == null) return false; aWithdrawal -= _balance; if(_debitPolicy(aWithdrawal)) { _balance = 0; return true; } else { return false; } } } protected DebitPolicy _debitPolicy; protected decimal _balance; }
The DebitPolicy delegate is declared in the Account class and is used to manage overdrafts. The DebitPolicy delegate is invoked by the Account.Withdrawal method when a withdrawal is larger than the current balance. If the overdraft is allowed, the delegate returns true; if denied, the delegate returns false. Because the overdraft debit policy is managed via delegates, multiple types of overdraft management can be defined, allowing different accounts to use different debit management policies while maintaining the same code in the Account class.
One overdraft policy class is shown in the following code. The OverdraftProtection class accepts an initial loan amount as a parameter during construction, and tracks the amount of overdraft protection currently available.
public class OverdraftProtection { public OverdraftProtection(decimal initialLoan) { _availableLoan = initialLoan; _currentLoan = 0; } public decimal AvailableLoan { set { _availableLoan = value; } get { return _availableLoan; } } public decimal CurrentLoan { set { _currentLoan = value; } get { return _currentLoan; } } // Method used with instances of WithdrawalPolicyDelegate // to manage debits to an account. Overdrafts are covered // up to a fixed amount specified at construction. public bool HandleDebit(decimal debit) { if(debit > 0) { if(debit <= _availableLoan) { _availableLoan -= debit; _currentLoan += debit; } else return false; } return true; } // Loan available for use in case of overdraft protected decimal _availableLoan; // Current overdraft loan amount protected decimal _currentLoan; }
The OverdraftProtection.HandleDebit method matches the signature of the DebitPolicy delegate and is used to manage overdraft debit requests. The OverdraftProtection and Account classes are used as follows:
OverdraftProtection op = new OverdraftProtection(500); Account.DebitPolicy policy; policy = new Account.DebitPolicy(op.HandleDebit); Account myAccount = new Account(100, policy);
In this sample code, an OverdraftProtection object named op is created, with an initial overdraft loan limit of $500. Next an instance of the DebitPolicy delegate is created, using op.HandleDebit as the target method. Last the Account object is created, with an initial balance of $100 and a reference to the newly created policy delegate. This code is part of the Bank example on the companion CD, which contains code that allows you to interactively overdraw the account and cause the overdraft debit policy delegate to be invoked.