CHAPTER 22

image

Delegates, Anonymous Methods, and Lambdas

Delegates are similar to interfaces, in that they specify a contract between a caller and an implementer. Rather than specifying a set of methods, a delegate merely specifies the form of a single function. Also, interfaces are created at compile time and are a fixed aspect of a type, whereas delegates are created at runtime and can be used to dynamically hook up callbacks between objects that were not originally designed to work together.

Delegates are used as the basis for events in C#, which are the general-purpose notification mechanisms used by the .NET Framework, and they are the subject of the next chapter.

Anonymous methods and lambdas provide two alternatives to specify the code that is hooked up to a delegate.

Using Delegates

The specification of the delegate determines the form of the function. 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. The sorting could be controlled 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 item1 = m_arr[x];
       object item2 = m_arr[y];
       int order = compare(item1, item2);
    }
    object[] m_ arr = new object[1];    // items in the collection
}
public class Employee
{
    public Employee(string name, int id)
    {
       m_name = name;
       m_id = id;
    }
    public static int CompareName(object obj1, object obj2)
    {
       Employee emp1 = (Employee) obj1;
       Employee emp2 = (Employee) obj2;
       return(String.Compare(emp1. m_name, emp2. m_name));
    }
    public static int CompareId(object obj1, object obj2)
    {
       Employee emp1 = (Employee) obj1;
       Employee emp2 = (Employee) obj2;
       if (emp1. m_id > emp2. m_id)
       {
            return(1);
       }
       else if (emp1. m_id < emp2. m_id)
       {
            return(−1);
       }
       else
       {
            return(0);
       }
    }
    string m_name;
    int m_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, a delegate can be passed in that describes the ordering that should be used, and the sort function will do the sorting.1

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,2 there needs to be some way to communicate some state information to the function so that it knows what object the event corresponds to. 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 there is no need for an extra parameter to carry the instance information. This is also a typesafe mechanism, because the instance is specified at the same time the function to call is specified.

using System;
public class User
{
    string m_name;
    public User(string name)
    {
       m_name = name;
    }
    public void Process(string message)
    {
       Console.WriteLine("{0}: {1}", m_name, message);
    }
}
class Test
{
    delegate void ProcessHandler(string message);
    public static void Main()
    {
       User user = new User("George");
       ProcessHandler handler = new ProcessHandler(user.Process);
       handler("Wake Up!");
    }
}

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

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 to take two delegates and return one that encapsulates both or to remove a delegate from another.

To combine two delegates, the Delegate.Combine() function is used. The last example can be easily modified to call more than one function.

using System;
public class User
{
    string m_name;
    public User(string name)
    {
       m_name = name;
    }
    public void Process(string message)
    {
       Console.WriteLine("{0}: {1}", m_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 handler = new ProcessHandler(user.Process);
       handler = (ProcessHandler) Delegate.Combine(handler, new ProcessHandler(Process));
       handler("Wake Up!");
    }
}

Invoking handler now calls both delegates.

There are a couple of problems with this approach, however. The first is that it’s not simple to understand. More importantly, however, is that it isn’t typesafe at compile time; Delegate.Combine() both takes and returns the type Delegate, so there’s 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. The call in the example is modified to the following:

handler += new ProcessHandler(Process);

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

handler("Wake Up!");

the following can be used:

foreach (ProcessHandler subHandler in handler.GetInvocationList())
{
    try
    {
       subHandler("Wake Up!");
    }
    catch (Exception e)
    {
       // log the exception here. . .
    }
}

code like this could also be used to implement “black-ball” voting, where all delegates could be called once to see whether they were able to perform a function and then called 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 will be covered in Chapter 23.

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 would be nicer if they didn’t have to do that, and that can be done 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)
    {
       m_name = name;
       m_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. m_name, emp2. m_name));
    }
    public static int CompareId(object obj1, object obj2)
    {
       Employee emp1 = (Employee) obj1;
       Employee emp2 = (Employee) obj2;
       if (emp1. m_id > emp2. m_id)
       {
            return(1);
       }
       else if (emp1. m_id < emp2. m_id)
       {
            return(−1);
       }
       else
       {
            return(0);
       }
    }
    string m_name;
    int m_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.

Anonymous Methods

Delegates work very nicely for scenarios where the code to execute is known ahead of time, such as a button event handler. But they don’t work very well in some other situations. For example, if I have a list of employees and I want to find the first one who is older than 40, I need to do something like the following:

class Employee
{
    public Employee(string name, int age, decimal salary)
    {
       Name = name;
       Age = age;
       Salary = salary;
    }
    public string Name { get; set; }
    public int Age { get; set; }
    public decimal Salary { get; set; }
}
class EmployeeFilterByAge
{
    int m_age;
    public EmployeeFilterByAge(int age)
    {
       m_age = age;
    }
    public bool OlderThan(Employee employee)
    {
       return employee.Age > m_age;
    }
}

The OlderThan() method can be passed to a selection method,3 which will return true if the employee is older than the specified age. This can then be used in the following code:

List <Employee> employees = new List <Employee> ();
employees.Add(new Employee("John", 33, 22000m));
employees.Add(new Employee("Eric", 42, 18000m));
employees.Add(new Employee("Michael", 33, 19500m));
EmployeeFilterByAge filterByAge = new EmployeeFilterByAge(40);
int index = employees.FindIndex(filterByAge.OlderThan);

This does what I want, but I have to create the EmployeeFilterByAge class, which takes some extra time and effort. I would prefer a way to express the comparison that I want in fewer lines of code and in a way that makes it more obvious what I am doing. Something like this:

int index = employees.FindIndex(
    delegate(Employee employee)
    {
       return employee.Age > 40;
    });

The compiler will do the heavy lifting, converting the few lines of code here into a separate class with a delegate that can be called directly. This construct is known as an anonymous delegate and was a popular addition to C# 2.0. There is, however, quite a bit of extra syntax to simply express the condition.

employee.Age > 40

This was addressed in C# 3.0, with the addition of lambdas.

Lambdas

The term lambda comes from a mathematical construct known as the lambda calculus, which is used in many functional programming languages. It provides a more concise way of expressing a function. It simplifies the declaration of the function to the following:

(parameters) => expression

The following method takes an integer parameter and returns a value that is one greater:

int AddOne(int x)
{
    return x + 1;
}

Expressed as a lambda, this becomes the following:

(int x) => { return x + 1; }

The lambda syntax supports a few simplifications.4 First, if the type of the parameter can be determined based on the context where the lambda is written, the type can be omitted.

(x) => { return x + 1; }

If there is only one parameter to the function, the parentheses can be omitted.

x => { return x + 1; }

Finally, if the only thing the body of the lambda does is return a value, the curly brackets and return statement can be omitted.

x => x + 1

This gives you the simplest version of the lambda expression. You can revisit the earlier call to FindIndex() using a lambda instead of an anonymous delegate.

int index = employees.FindIndex(employee => employee.Age > 40);

image Note  For many more lambda examples, see Chapter 29.

Implementation

The implementation of the previous lambda is quite simple; the compiler creates the following method:

private static bool MyAgeFilter(Employee employee)
{
    return employee.Age > 40;
}
It’s not going to call it MyAgeFilter, however. It will use a generated name like ' <Example> b__0’.

Variable Capturing

Consider the following code:

List <Employee> employees = new List <Employee> ();
employees.Add(new Employee("John", 33, 22000m));
employees.Add(new Employee("Eric", 42, 18000m));
employees.Add(new Employee("Michael", 32, 19500m));

int ageThreshold = 40;
Predicate <Employee> match = e => e.Age > ageThreshold;
ageThreshold = 30;

int index = employees.FindIndex(match);
Console.WriteLine(index);

Does this code locate John because his age is greater than 40, or does it locate Eric because his age is greater than 32?

The answer to that question depends on what the usage of ageThreshold means in the lambda expression. In lambda expressions, the variable is captured, which means that the expression uses the value of ageThreshold at the time that the expression is evaluated, not the value of it when the lambda was defined. It therefore locates Eric. This distinction is most important in cases where the lambda escapes the current scope.

To capture the variable, it creates a class.

class DisplayClass
{
    public int ageThreshold;
    public bool MyPredicate(Employee employee)
    {
       return employee.Age > ageThreshold;
    }
}

The usage is roughly5 the following:

DisplayClass d = new DisplayClass ();
d.ageThreadhold = 40;
Predicate <Employee> match = d.MyPredicate;
d.ageThreshold = 30;

int index = employees.FindIndex(match);

Guidelines

I recommend the following guidelines for lambdas.

Parameter Naming

Lambda parameters should be named with the same care you would use for other parameters. Compare the readability of this lambda:

int index = employees.FindIndex(employee => employee.Age > 40);

with this one:

int index = employees.FindIndex(x => x.Age > 40);

The first is considerably easier to understand. This is especially important when an expression involves more than one lambda.6

Method, Anonymous Delegate, or Lambda?

C# provides three choices; which of these constructs should you use?

The lambda syntax is so much nicer than the anonymous delegate one that I see no reason to ever use the anonymous delegate one, which leaves two choices. Choosing between them is an aesthetic choice, and there are differing opinions. This is what I recommend:

  • Use lambdas any place you are passing code as a parameter to another method.
  • Use methods to hook to events.

There are two reasons for the second guideline. The first has to do with unsubscribing from an event.

image Note  It would be helpful to read Chapter 23 before you read this section.7

Consider the following:

Console.CancelKeyPress + = (o, args) =>
    {
       Console.WriteLine("Boo");
       args.Cancel = true;
    };
Console.CancelKeyPress - = (o, args) =>
    {
       Console.WriteLine("Boo");
       args.Cancel = true;
    };

This looks like the standard event pattern; the first call subscribes to the event, and the second one unsubscribes. It is a bit ugly because you need to write the whole lambda over, but it has a more serious problem.

It doesn’t work. While the two lambdas appear to be identical, they do not generate the same instance of a delegate, and therefore the statement to unsubscribe to the event does nothing,8 which is bad.

The second reason is readability. Event handlers tend to have more code in them than the simple expressions in this chapter, which makes the extra syntax of a method less important. It is also nice to be able to see what the type of the parameter is when reading the code.9 If you have a few event handlers to set up at the beginning of a method, you end up with all of your event handling code in a single method, which doesn’t make it very readable. Finally, I find it much easier to read the subscription line without all the handler code getting in the way. The following:

Console.CancelKeyPress + = HandleControlC;

is much easier to read than the lambda equivalent.

1 Well, it would if it were actually implemented.

2 You might ask, “What about member function pointers?” Member functions do indeed do something similar, but the syntax is rather opaque. Delegates in C# are both easier to use and more functional.

3 In this usage, the method is known as a predicate.

4 I’m not sure I like the simplifications. It is simpler to write the code, but the form (int x) => { return x + 1; } is clearer than x => x + 1, at least initially.

5 The names have been changed to improve clarity.

6 You will likely see lots of code that just uses x for lambda parameters.

7 I’ll wait for you here.

8 You can get the behavior you want by assigning the lambda to a variable and using that same variable to subscribe and unsubscribe, but at that point you’re pretty close to using a method.

9 Yes, you can specify a parameter name in the lambda, but almost nobody does.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.81.200