[C# Programming] Delegation, lambda expressions and events

Table of Contents

1. Delegate and lambda expression

1.1 Overview of delegation

1.2 Declaration of delegate type

1.3 Instantiation of delegates

1.4 Internal mechanism of delegation

1.5 Lambda expressions

1.6 Statement lambda

1.7 Expression lambda

1.8 Lambda expression

1.9 General delegation

1.10 Delegates have no structural equality

1.11 Internal mechanism of Lambda expressions and anonymous methods

1.12 External variables

1.13 CIL implementation of external variables

2. Events

2.1 Multicast delegation

2.2 Using multicast delegation to encode the Observer pattern

2.3 Delegate operator

2.4 Error handling

2.5 The role of events

2.6 Statement of events

2.7 Coding standards

2.8 Internal mechanism of events

2.9 Implementation of custom events


1. Delegation and lambda expression

1.1 Commission Overview

C/C++ uses “function pointers” to pass a method reference as an actual parameter to another method. C# provides the same functionality using delegates. Delegates allow you to capture a reference to a method, pass it around like any other object, and call the captured method like any other method. For example:

public static void BubbleSort(int[] items, ComparisonHandler comparisonMethod)
{
    int i, j, temp;
    if(comparisonMethod == null)
        throw new ArgumentNullException("comparisonMethod");
    if(items == null)
        return;
    for(i = items.Length - 1; i >= 0; i--)
    {
        for(j = 1; j <= i; j + + )
        {
            if(comparisonMethod(items[j - 1], items[j]))
            {
                temp = items[j - 1];
                items[j - 1] = items[j];
                items[j] = temp;
            }
        }
    }
}

1.2 Declaration of delegate type

To declare a delegate, use the delegate keyword, followed by the method’s signature. The signature of this method is the signature of the method referenced by the delegate. For example:

public delegate bool ComparisonHandler(int first, int second);

Delegates can be nested within classes. If a delegate declaration appears inside another class, the delegate type becomes a nested type.

class DelegateSample
{
    public delegate bool ComparisonHandler(int first, int second);
}

1.3 Instantiation of Delegation

In order to instantiate a delegate, a method is required that matches the signature of the delegate type itself. For example:

public delegate bool ComparisonHandler(int first, int second);
class DelegateSample
{
    public static void BubbleSort(int[] items, ComparisonHandler comparisonMethod)
    {
    //…
    }
    public static bool GreaterThan(int first, int second)
    {
        return first > second;
    }
    static void Main()
    {
        int i;
        int[] items = new int[5];
        for(i = 0; i < items.Length; i + + )
        {
            Console.Write("Enter an integer: ");
            items[i] = int.Parse(Console.ReadLine());
        }
        BubbleSort(items, GreaterThan);
        for(i = 0; i < items.Length; i + + )
            Console.WriteLine(items[i]);
    }
}

1.4 Internal Mechanism of Delegation

Delegates are special classes, delegate types in .Net always derive from System.MulticastDelegate, which in turn derives from System.Delegate.

The C# compiler does not allow the declaration of classes derived directly or indirectly from System.Delegate or System.MulicastDelegate.

1.5 Lambda expression

C# 2.0 introduced a very streamlined syntax for creating delegates, and the related features are called anonymous methods. A C# 3.0-related feature is called Lambda expressions. These two syntaxes are collectively called anonymous functions. Lambda expressions themselves are divided into two types: statement lambdas and expression lambdas.

1.6 Statement lambda

A statement lambda consists of a formal parameter list, followed by the lambda operator =>, and then a code block. For example:

public class DelegateSample
{
    //…..
    public static void Main()
    {
        int i;
        int[] items = new int[5];
        for(i = 0; i < items.Length; i + + )
        {
            Console.Write("Enter an integer: ");
            items[i] = int.Parse(Console.ReadLine());
        }
        BubbleSort(items, (int first, int second) => { return first < second;});
        for(i = 0; i < items.Length; i + + )
        {
            Console.WriteLine(items[i]);
        }
    }
}

While the compiler can infer the type from the delegate that the lambda expression is converted into, all lambdas do not need to explicitly declare parameter types. C# requires that the Lambda type be specified explicitly when the type cannot be inferred. As long as a Lambda parameter type is explicitly specified, all parameter types must be explicitly specified, for example:

public static void ChapterMain()
{
    int i;
    int[] items = new int[5];
    for(i = 0; i < items.Length; i + + )
    {
        Console.Write("Enter an integer:");
        items[i] = int.Parse(Console.ReadLine());
    }
    DelegateSample.BubbleSort(items, (first, second) => { return first < second;});
    for(i = 0; i < items.Length; i + + )
    {
        Console.WriteLine(items[i]);
    }
}

When there is only a single parameter and the type can be inferred, such a lambda expression can omit the parentheses surrounding the parameter list. If the lambda has no parameters, has more than one parameter, or explicitly specifies a single parameter of type, then the parameter list must be enclosed in parentheses. For example:

public class Program
{
    public static void ChapterMain()
    {
        IEnumerable<Process> processes = Process.GetProcesses().Where(
           process => { return process.WorkingSet64 > 1000000000; });
    }
}

Statement Lambda without parameters

public static void ChapterMain()
{
    Func<string> getUserInput = () =>
    {
        string input;
        do
        {
            input = Console.ReadLine();
        }
        while(input.Trim().Length == 0);
        return input;
    };
}

1.7 Expression lambda

An expression lambda only has the expression to return, no statement block at all. For example:

public static void Main()
{
    int i;
    int[] items = new int[5];
    for(i = 0; i < items.Length; i + + )
    {
        Console.Write("Enter an integer:");
        items[i] = int.Parse(Console.ReadLine());
    }
    DelegateSample.BubbleSort(items, (first, second) => first < second);
    for(i = 0; i < items.Length; i + + )
    {
        Console.WriteLine(items[i]);
    }
}

Like null literals, anonymous functions are not associated with any type. Its type is determined by the type it is converted to. So you cannot use the typeof() operator on an anonymous method. In addition, GetType can only be called after converting the anonymous method to a specific type

1.8 Lambda expression

1.9 General Delegation

.Net 3.5 includes a common set of delegates. The System.Func series delegates represent methods that return values, while the System.Action series delegates represent methods that return void. The last parameter of the Func delegate is always the return type of the delegate, and the other parameters in turn correspond to the types of the delegate parameters.

//public delegate void Action();
//public delegate void Action<in T>(T arg);
//public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
//public delegate void Action<in T1, in T2, in T3>(T1 arg1, T2 arg2, T3 arg3);
//public delegate void Action<in T1, in T2, in T3, in T4>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
// ...
//public delegate void Action<in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10, in T11, in T12, in T13, in T14, in T15, in T16>(
// T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12,
// T13 arg13, T14 arg14, T15 arg15, T16 arg16);

//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);
//public delegate TResult Func<in T1, in T2, in T3, out TResult>(T1 arg1, T2 arg2, T3 arg3);
//public delegate TResult Func<in T1, in T2, in T3, in T4, out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4);
// ...
//public delegate TResult Func< in T1, in T2, in T3, in T4, in T5, in T6, in T7, in T8, in T9, in T10, in T11, in T12, in T13, in T14, in T15, in T16,
// out TResult>(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6, T7 arg7, T8 arg8, T9 arg9, T10 arg10, T11 arg11, T12 arg12,
// T13 arg13, T14 arg14, T15 arg15, T16 arg16);

In many cases, the Func delegate added in .NET 3.5 eliminates the need to define your own delegate type altogether. For example:

public static void BubbleSort(int[] items, Func<int, int, bool> comparisonMethod)

1.10 Delegates have no structural equality

.NET delegate types do not have structural equality. In other words, you cannot convert an object of a certain delegate type to an unrelated delegate type, even if the formal parameters and return types of the two delegate types are exactly the same. However, with the support for mutability added in C# 4.0, reference conversions between certain reference types are possible.

public static void ChapterMain()
{
    // Contravariance
    Action<object> broadAction =
        (object data) =>
        {
            Console.WriteLine(data);
        };
    Action<string> narrowAction = broadAction;
    // Covariance
    Func<string> narrowFunction =
        () => Console.ReadLine();
    Func<object> broadFunction = narrowFunction;
    // Contravariance and covariance combined
    Func<object, string> func1 =
        (object data) => data.ToString();
    Func<string, object> func2 = func1;
}

1.11 The internal mechanism of Lambda expressions and anonymous methods

When the compiler encounters an anonymous method, it converts it into a special hidden class, field, and method. For example:

public static void Main()
{
    int i;
    int[] items = new int[5];
    for(i = 0; i < items.Length; i + + )
    {
        Console.Write("Enter an integer:");
        items[i] = int.Parse(Console.ReadLine());
    }
    BubbleSort(items, DelegateSample.__AnonymousMethod_00000000);
    for(i = 0; i < items.Length; i + + )
    {
        Console.WriteLine(items[i]);
    }
}
private static bool __AnonymousMethod_00000000(int first, int second)
{
    return first < second;
}

1.12 External variables

Local variables declared outside a Lambda expression are called external variables of the expression. When the lambda body uses an external variable, the variable is said to be captured by the lambda

public static void ChapterMain()
{
    int i;
    int[] items = new int[5];
    int comparisonCount = 0;
    for(i = 0; i < items.Length; i + + )
    {
        Console.Write("Enter an integer:");
        items[i] = int.Parse(Console.ReadLine());
    }
    DelegateSample.BubbleSort(items, (int first, int second) => {comparisonCount + + ; return first < second; });
    for(i = 0; i < items.Length; i + + )
Console.WriteLine(items[i]);
    Console.WriteLine("Items were compared {0} times.", comparisonCount);
}

If a lambda expression captures an external variable, a delegate created based on that expression may have a longer lifetime than the local variable. In this case, the lifetime of the captured variable becomes longer.

1.13 CIL implementation of external variables

Local variables declared outside a Lambda expression are called external variables of the expression. When the lambda body uses an external variable, the variable is said to be captured by the lambda

private sealed class __LocalsDisplayClass_00000001
{
    public int comparisonCount;
    public bool __AnonymousMethod_00000000(int first, int second)
    {
        comparisonCount + + ;
        return first < second;
    }
}
public static void Main()
{
    int i;
    __LocalsDisplayClass_00000001 locals = new __LocalsDisplayClass_00000001();
    locals.comparisonCount = 0;
    int[] items = new int[5];
    for(i = 0; i < items.Length; i + + )
    {
         Console.Write("Enter an integer:");
         items[i] = int.Parse(Console.ReadLine());
    }
    DelegateSample.BubbleSort(items, locals.__AnonymousMethod_00000000);
    for(i = 0; i < items.Length; i + + )
Console.WriteLine(items[i]);
    Console.WriteLine("Items were compared {0} times.", locals.comparisonCount);
}

2. Event

2.1 Multicast Delegation

The delegate itself is the basic unit of the publish-subscribe pattern

A delegate value can reference a series of methods, which will be called sequentially. Such a delegation is called a multicast delegation. Using multicast delegation, notifications of a single event can be published to multiple subscribers

2.2 Use multicast delegation to encode the Observer pattern

Define subscriber method

class Cooler
{
    public Cooler(float temperature) {Temperature = temperature;}
    public float Temperature { get; set; }
    public void OnTemperatureChanged(float newTemperature)
    {
        if(newTemperature > Temperature)
            System.Console.WriteLine("Cooler: On");
        else
            System.Console.WriteLine("Cooler: Off");
    }
}
class Heater
{
    public Heater(float temperature) {Temperature = temperature;}
    public float Temperature { get; set; }
    public void OnTemperatureChanged(float newTemperature)
    {
        if(newTemperature < Temperature)
            System.Console.WriteLine("Heater: On");
        else
            System.Console.WriteLine("Heater: Off");
    }
}

Define publisher

public class Thermostat
{
    public Action<float> OnTemperatureChange { get; set; }
    public float CurrentTemperature
    {
        get { return _CurrentTemperature; }
        set
        {
            if(value != CurrentTemperature)
            {
                _CurrentTemperature = value;
            }
        }
    }
    private float _CurrentTemperature;
}

Connect publishers and subscribers

public static void Main()
{
    Thermostat thermostat = new Thermostat();
    Heater heater = new Heater(60);
    Cooler cooler = new Cooler(80);
    string temperature; // Using C# 2.0 or later syntax.
    thermostat.OnTemperatureChange + = heater.OnTemperatureChanged;
    thermostat.OnTemperatureChange + = cooler.OnTemperatureChanged;
    Console.Write("Enter temperature: ");
    temperature = Console.ReadLine();
    thermostat.CurrentTemperature = int.Parse(temperature);
}

Call delegate

public class Thermostat
{
    // Define the event publisher
    public Action<float> OnTemperatureChange { get; set; }
    public float CurrentTemperature
    {
        get { return _CurrentTemperature; }
        set
        {
            if(value != CurrentTemperature)
            {
                _CurrentTemperature = value;
                // INCOMPLETE: Check for null needed Call subscribers
                OnTemperatureChange(value);
            }
        }
    }
    private float _CurrentTemperature;
}

Check for null value

public static void ChapterMain()
{
    Thermostat thermostat = new Thermostat();
    Heater heater = new Heater(60);
    Cooler cooler = new Cooler(80);
    Action<float> delegate1;
    Action<float> delegate2;
    Action<float> delegate3;
    // use Constructor syntax for C# 1.0.
    delegate1 = heater.OnTemperatureChanged;
    delegate2 = cooler.OnTemperatureChanged;
    Console.WriteLine("Invoke both delegates:");
    delegate3 = delegate1;
    delegate3 + = delegate2;
    delegate3(90);
    Console.WriteLine("Invoke only delegate2");
    delegate3 -= delegate1;
    delegate3(30);
}

2.3 Delegation Operator

To merge multiple subscribers, use the + = operator. This operator takes the first delegate and adds the second delegate to the delegate chain. To remove a delegate from the delegate chain, use the -= operator

public static void ChapterMain()
{
    Thermostat thermostat = new Thermostat();
    Heater heater = new Heater(60);
    Cooler cooler = new Cooler(80);
    Action<float> delegate1;
    Action<float> delegate2;
    Action<float> delegate3;
    // use Constructor syntax for C# 1.0.
    delegate1 = heater.OnTemperatureChanged;
    delegate2 = cooler.OnTemperatureChanged;
    Console.WriteLine("Invoke both delegates:");
    delegate3 = delegate1;
    delegate3 + = delegate2;
    delegate3(90);
    Console.WriteLine("Invoke only delegate2");
    delegate3 -= delegate1;
    delegate3(30);
}

We can also merge delegates using + and –

public static void ChapterMain()
{
    Thermostat thermostat = new Thermostat();
    Heater heater = new Heater(60);
    Cooler cooler = new Cooler(80);
    Action<float> delegate1, delegate2, delegate3;
    delegate1 = heater.OnTemperatureChanged;
    delegate2 = cooler.OnTemperatureChanged;
    Console.WriteLine("Combine delegates using + operator:");
    delegate3 = delegate1 + delegate2;
    delegate3(60);
    Console.WriteLine("Uncombine delegates using - operator:");
    delegate3 = delegate3 - delegate2;
    delegate3(60);
}

Regardless of + – or + = or -=, they are implemented internally using the static methods system.Delegate.combine() and System.Delegate.Remove(). Combine will connect the two parameters and connect the call lists of the two delegates in order, and Remove will delete the delegate specified by the second parameter.

Sequential call

2.4 Error Handling

If an exception occurs to a subscriber, subsequent subscribers in the chain will not receive notifications.

public static void Main()
{
    Thermostat thermostat = new Thermostat();
    Heater heater = new Heater(60);
    Cooler cooler = new Cooler(80);
    string temperature;
    thermostat.OnTemperatureChange + =heater.OnTemperatureChanged;
    thermostat.OnTemperatureChange + =
        (newTemperature) =>
    {
        throw new InvalidOperationException();
    };
    thermostat.OnTemperatureChange + =
        cooler.OnTemperatureChanged;

    Console.Write("Enter temperature: ");
    temperature = Console.ReadLine();
    thermostat.CurrentTemperature = int.Parse(temperature);
}

To avoid this problem, you must manually traverse the delegate chain and call the delegates individually

public class Thermostat {
    public Action<float> OnTemperatureChange;
    public float CurrentTemperature
    {
        get { return _CurrentTemperature; }
        set
        {
            if(value != CurrentTemperature) {
                _CurrentTemperature = value;
                Action<float> onTemperatureChange = OnTemperatureChange;
                if (onTemperatureChange != null) {
                    List<Exception> exceptionCollection = new List<Exception>();
                    foreach(Action<float> handler in onTemperatureChange.GetInvocationList())
                    {
                        try {
                            handler(value);
                        }
                        catch(Exception exception) {
                            exceptionCollection.Add(exception);
                        }
                    }
                    if(exceptionCollection.Count > 0)
                        throw new AggregateException( "There were exceptions thrown by " + "OnTemperatureChange Event subscribers.", exceptionCollection);
                }
            }
        }
    }
    private float _CurrentTemperature;
}

2.5 The role of events

Encapsulated subscription: Events provide support for assignment operators only for objects inside the containing class.

public static void Main()
{
    Thermostat thermostat = new Thermostat();
    Heater heater = new Heater(60);
    Cooler cooler = new Cooler(80);
    string temperature;
    thermostat.OnTemperatureChange = heater.OnTemperatureChanged;
    // Bug: Assignment operator overrides // previous assignment.
    thermostat.OnTemperatureChange = cooler.OnTemperatureChanged;
    Console.Write("Enter temperature: ");
    temperature = Console.ReadLine();
    thermostat.CurrentTemperature = int.Parse(temperature);
}

Encapsulated Release: Events ensure that only the containing class can trigger exceptions

public static void ChapterMain()
{
    //……..
    thermostat.OnTemperatureChange + = heater.OnTemperatureChanged;
    thermostat.OnTemperatureChange + =cooler.OnTemperatureChanged;
    // Bug: Should not be allowed
    thermostat.OnTemperatureChange(42);
}

2.6 Event Statement

C# uses events to solve two major problems with delegation. Event defines a new member type, for example:

public class Thermostat
{
    public class TemperatureArgs : System.EventArgs
    {
        public TemperatureArgs(float newTemperature)
        {
            NewTemperature = newTemperature;
        }
        public float NewTemperature { get; set; }
    }
    // Define the event publisher
    public event EventHandler<TemperatureArgs> OnTemperatureChange = delegate { };
    public float CurrentTemperature
    {
        get { return _CurrentTemperature; }
        set { _CurrentTemperature = value; }
    }
    private float _CurrentTemperature;
}

After adding the keyword event, it will be prohibited to use the assignment operator for a public delegate field. Only the containing class can call the notification delegate issued to all delegates; delegate{} represents an empty delegate, representing a collection of zero listeners. By assigning an empty delegate, you can raise an event without having to check if there is a listener

2.7 Coding Standard

In order to get the desired functionality, you need to declare the original delegate variable as a field and then add the event keyword. In order to comply with C# coding standards, the original delegate needs to be replaced with the new delegate type EventHandle, for example:

public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e)
    where TEventArgs : EventArgs;
  • The first parameter sender is of object type, which contains a reference to the object calling the delegate (null for static events)
  • The second parameter is of type System.EventArgs, or is derived from System.EventArgs but contains additional data about the event.

Trigger event notification

public float CurrentTemperature
{
    get { return _CurrentTemperature; }
    set {
        if(value != CurrentTemperature)
        {
            _CurrentTemperature = value;
            // If there are any subscribers, notify them of changes in temperature by invoking said subcribers
            OnTemperatureChange?.Invoke( this, new TemperatureArgs(value));
         }
    }
}

Specification:

  • To check that the value of a delegate is not null before calling it
  • Do not pass null value for sender of non-static events
  • To pass null value for static event sender
  • Do not pass null value for eventArgs. Use EventHandler delegate type for events.
  • To use System.EventArgs type or its derived type for TEventArgs
  • Consider using a subclass of System.EventArgs as the argument type for events unless you are absolutely certain that the event will never need to carry any data.

2.8 Internal Mechanism of Events

Event restrictions external classes can only add subscription methods to the publisher through + = and unsubscribe using -=. Nothing else is allowed to be done. Additionally, it prohibits any class other than the containing class from calling the event. To do this, the compiler will get the public delegate with the event modifier and declare the delegate as private. In addition, it will add two methods and a special event block.

public class Thermostat {
    // ...
    // Declaring the delegate field to save the list of subscribers.
    private EventHandler<TemperatureArgs> _OnTemperatureChange;
    public void add_OnTemperatureChange(EventHandler<TemperatureArgs> handler) {
        System.Delegate.Combine(_OnTemperatureChange, handler);
    }
    public void remove_OnTemperatureChange( EventHandler<TemperatureArgs> handler) {
        System.Delegate.Remove(_OnTemperatureChange, handler);
    }
    //public event EventHandler<TemperatureArgs> OnTemperatureChange
    //{
    // add
    // {
    // add_OnTemperatureChange(value);
    // }
    // remove
    // {
    // remove_OnTemperatureChange(value);
    // }
    //}
    public class TemperatureArgs : System.EventArgs {
        public TemperatureArgs(float newTemperature) {}
    }
}

2.9 Implementation of custom events

The code generated by the compiler for += and -= can be customized, e.g.

public class Thermostat {
    public class TemperatureArgs : System.EventArgs
    {
        //….
    }
    // Define the delegate data type
    public delegate void TemperatureChangeHandler(object sender, TemperatureArgs newTemperature);
    // Define the event publisher
    public event TemperatureChangeHandler OnTemperatureChange
    {
        add
        {
            _OnTemperatureChange = (TemperatureChangeHandler)System.Delegate.Combine(value, _OnTemperatureChange);
        }
        remove
        {
            _OnTemperatureChange = (TemperatureChangeHandler)System.Delegate.Remove(_OnTemperatureChange, value);
        }
    }
    protected TemperatureChangeHandler _OnTemperatureChange;
    public float CurrentTemperature
    {
        //......
    }
}