C# delegates, lambda expressions and events

1. Reference method

Delegates are the .NET equivalent of addressing methods. In C++, a function pointer is nothing more than a pointer to a memory location, which is not type safe. We can’t tell what this pointer actually points to, let alone items like parameters and return types. A .NET delegate is completely different; a delegate is a type-safe class that defines the return type and the types of the parameters. A delegate class not only contains references to methods, but can also contain references to multiple methods.

Lambda expressions are directly related to delegates. When the parameter is a delegate type, you can use a lambda expression to implement the method referenced by the delegate.

2. Delegation

A delegate is just a special type of object in that all the objects we defined before contain data, whereas a delegate contains only the address of one or more methods.

In C#, a delegate (Delegate) is a reference type that allows developers to pass, store, and call methods as parameters. Delegates provide a flexible way to handle scenarios such as callback functions, event handling, and asynchronous programming.

The following is a detailed introduction to delegation in C#:

2.1 Define the delegate type

In C#, you first need to define the delegate type, which defines the signatures of the methods that the delegate can refer to. A delegate definition is similar to a method declaration, but without an implementation body. Delegate definitions can be placed inside namespaces, classes, or structures. For example:

// defines a delegate named MyDelegate, which can refer to a method with an int type parameter and no return value
delegate void MyDelegate(int x);

// Other examples:
// Define a delegate IntMethodInvoker, which represents a method with a parameter of type int and no return type
delegate void IntMethodInvoker(int x);
// Define a delegate TwoLongsOp, the method represented by this delegate has two long parameters, and the return type is double
delegate double TwoLongsOp(long first, long second);
// Define a delegate, the method it represents has no parameters and returns a string value
delegate string GetAString();

The above code defines a delegate named MyDelegate, which can refer to a method with one parameter of type int and no return value.

Note: Actually, “defining a delegate” means “defining a new class”. A delegate is implemented as a class derived from the base class System.MulticastDelegate, which in turn is derived from the base class System.Delegate. The C# compiler can recognize this class and use its delegate syntax, so we don’t need to know the specific implementation of this class. This is another example of C# working with base classes to make programming easier.

2.2 Create a delegate instance

Once a delegate type is defined, instances of the delegate can be created. The delegate instance will reference one or more methods with signatures matching the delegate. An instance of a delegate can be created using the new keyword and the constructor of the delegate type. For example:

MyDelegate del = new MyDelegate(MyMethod);

The preceding code creates a delegate instance named del that references a method named MyMethod that has the same delegate type as MyDelegate matching signature.

The code snippet below illustrates how to use delegates. Here’s a rather verbose way of calling the ToString() method on an int value:

private delegate string GetAString();
public static void Main()
{<!-- -->
    int x = 40;
    GetAString firstStringMethod = new GetAString(x.ToString);
    Console.WriteLine($"String is {<!-- -->firstStringMethod()}");
    // The above statement is equivalent to
    // Console. WriteLine($"String is {x. ToString()}");
}

In this code, a delegate of type GetAString is instantiated and initialized to refer to the ToString() method of the integer variable x. In C#, a delegate syntactically always accepts a constructor with one parameter, which is the method referenced by the delegate. This method must match the signature when the delegate was originally defined. So in this example, if you don’t initialize the firstStringMethod variable with a method that takes no parameters and returns a string, it will generate a compile error. Note that because int.ToString() is an instance method (not a static method), the instance (x) and method name need to be specified to properly initialize the delegate.

The next line of code uses this delegate to display the string. In any code, you should provide the name of the delegate instance, followed by parentheses with any equivalent parameters used when calling the method in the delegate. So in the code above, the Console.WriteLine() statement is exactly equivalent to the commented out line of code.

In fact, providing parentheses to the delegate instance is exactly the same as calling the delegate class’s Invoke() method. Because firstStringMethod is a variable of the delegate type, the C# compiler will replace firstStringMethod() with firstStringMethod.Invoke().

firstStringMethod();
// firstStringMethod. Invoke();

To reduce typing, only the name of the address can be passed at each location where a delegate instance is required. This is called delegate inference. This C# feature is valid as long as the compiler can resolve a delegate instance to a specific type. The following example initializes the firstStringMethod variable of type GetAString with a new instance of the GetAString delegate:

GetAString firstStringMethod = new GetAString(x.ToString);

You can write code that does the same thing, just use the variable x to pass the method name to the variable firstStringMethod:

GetAString firstStringMethod = x.ToString;

The code created by the C# compiler is the same. Since the compiler detects the required delegate type with firstStringMethod, it creates an instance of the GetAString delegate type, passing the address of the method to the constructor with object x.

Note: When calling the above method name, the input form cannot be x.ToString() (do not enter the parentheses), nor can it be transferred to the delegate variable. Typing parentheses calls a method, and calling the x.ToString() method returns a String object that cannot be assigned to a delegate variable. Only the address of the method can be assigned to the delegate variable.

2.3 Delegated call

A delegate instance can invoke the method it refers to indirectly by calling the delegate. When invoking with a delegate, you can provide parameters just like calling a method. For example:

delegate void MyDelegate(int x);
MyDelegate del = new MyDelegate(MyMethod);
del(10);

The preceding code invokes the method referenced by the del delegate, passing the integer value 10 as a parameter to the method.

2.4 Delegated Multicast

A delegate can refer to one or more methods, which is called the delegated multicast (Multicast) function. One delegate instance can be combined with or removed from another by using the + and - operators. When combining multiple delegates, they will be invoked sequentially in the order they were added. For example:

MyDelegate del1 = new MyDelegate(Method1);
MyDelegate del2 = new MyDelegate(Method2);
MyDelegate del3 = del1 + del2; // composite delegate
del3(10); // call the combined delegate
del3 -= del2; // remove delegate

The above code combines two delegate instances del1 and del2 into a new delegate del3, and then calls the del3 delegate . Next, the del2 delegate is removed from the del3 delegate.

using System;

delegate void MyDelegate(string message);

class Program
{<!-- -->
    static void Main()
    {<!-- -->
        MyDelegate myDelegate = null;

        myDelegate += Method1;
        myDelegate += Method2;
        myDelegate += Method3;

        myDelegate("Hello, world!");

        Console. ReadLine();
    }

    static void Method1(string message)
    {<!-- -->
        Console.WriteLine("Method1: " + message);
    }

    static void Method2(string message)
    {<!-- -->
        Console.WriteLine("Method2: " + message);
    }

    static void Method3(string message)
    {<!-- -->
        Console.WriteLine("Method3: " + message);
    }
}

/*
Method1: Hello, world!
Method2: Hello, world!
Method3: Hello, world!
*/

In the above example, we defined a delegate type called MyDelegate that takes a string parameter and returns void. Then, we create a delegate instance of myDelegate and use the + = operator to assign three different methods Method1, Method2 and Method3 are added to the delegate.

When we call the myDelegate delegate and pass the string parameter "Hello, world!", the three methods added to the delegate will actually be called in turn, and the same The message is printed to the console.

2.5 Built-in delegate types

C# also provides some built-in delegate types that can be used directly without customizing the delegate types. Some commonly used built-in delegate types include:

  • Action: A delegate type that does not return a value.

  • Func: A delegate type with a return value.

  • Predicate: A delegate type that returns a Boolean value.

These built-in delegate types have different numbers and types of parameters, which can be selected according to needs.

Delegation is a powerful tool in C#, which can be used to implement various scenarios such as event processing, callback functions, and asynchronous programming. With delegation, methods can be treated as first-class objects and dynamically referenced, composed, and invoked at run time.

2.6 Action, Func and Predicate delegation

2.6.1 Action

In addition to defining a new delegate type for each parameter and return type, it is also possible to use Action and Func delegates. A generic Action delegate represents a reference to a method with a void return type. There are different variants of this delegate class, and up to 16 different parameter types can be passed. Action classes with no generic parameters can call methods with no parameters. Action calls a method with one parameter, Action calls a method with two parameters, Action call a method with 8 parameters.

Action<int> myAction = (x) => Console. WriteLine(x);
myAction(10); // call delegate, output 10

The above code defines an Action delegate instance named myAction, which refers to a method with one int parameter, and passes the Lambda The expression implements the method as an output parameter value.

You can use Action delegate to perform operations without return value, such as event handling, callback function, etc.

2.6.2 Func

The Func delegate can be used in a similar fashion. Func allows calling methods with return types. Similar to Action, Func also defines different variants, and up to 16 parameter types and one return type can be passed. Func delegate type can call a method with a return type and no parameters, Func calls a method with one parameter, Func calls a method with 4 parameters.

Func<int, int> myFunc = (x) => x * 2;
int result = myFunc(10); // call the delegate, the result is 20

The above code defines a Func delegate instance named myFunc, which references a int parameter and returns int. With a Lambda expression, we implement the method to multiply the input parameter by 2 and return the result.

You can use the Func delegate to perform operations with return values, such as calculations, conversions, and so on.

2.6.3 Predicate delegation

Predicate is a generic delegate type used to represent a method that returns a Boolean value and accepts a generic parameter. The Predicate delegate is usually used to match or filter elements in a collection or array.

The Predicate delegate is defined as follows:

public delegate bool Predicate<in T>(T obj);

where T is the type of element to be matched or filtered.

Here’s an example that demonstrates how to use the Predicate delegate for element filtering:

List<int> numbers = new List<int> {<!-- --> 1, 2, 3, 4, 5 };

Predicate<int> predicate = (x) => x % 2 == 0; // filter even numbers

List<int> evenNumbers = numbers. FindAll(predicate);

foreach (int number in evenNumbers)
{<!-- -->
    Console. WriteLine(number);
}

In the above example, we created a list of integers named numbers, and defined a Predicate delegate instance predicate, using Lambda expression to filter for even numbers.

We then use the FindAll method, which accepts a Predicate delegate as a parameter and returns a new list of elements that meet the criteria. We pass the predicate delegate to the FindAll method to filter out even numbers in the list.

Finally, we iterate over the filtered result list evenNumbers and print each even number.

2.7 Anonymous methods

In C#, an anonymous method is a method that can be defined directly in the code without an explicit method name, usually used to simplify the code or provide one-off logic when needed. An anonymous method can be used as an instance of a delegate type to be passed to a method or constructor that accepts delegate parameters.

Following is the general syntax of an anonymous method:

delegate (parameters)
{<!-- -->
    // code logic of anonymous method
};

In the definition of an anonymous method, you can specify a parameter list and a method body. Anonymous methods can use variables in the outer scope, and they have the same access rights as the outer scope.

Here’s an example that demonstrates how to use anonymous methods:

class Program
{<!-- -->
    delegate void MyDelegate(string message);

    static void Main()
    {<!-- -->
        MyDelegate myDelegate = delegate (string message) {<!-- --> Console. WriteLine("Hello, " + message); };

        myDelegate("world");

        Console. ReadLine();
    }
}

// Hello, world

In the above example, we defined a delegate type called MyDelegate that takes a string parameter and returns void. Then, we create a delegate instance of myDelegate and use anonymous methods to define the behavior of the delegate.

The code logic of the anonymous method is executed when the delegate is invoked. In this example, the anonymous method concatenates the passed string argument with a fixed prefix and prints the result to the console.

By using anonymous methods, we can define simple logic directly in the code without declaring named methods separately. This is great for simple logic that needs to be used once, reducing code size and improving readability. Anonymous methods are usually used in event processing, LINQ query, asynchronous programming and other scenarios.

There are two rules that must be followed when using anonymous methods.

  • A jump statement (break, goto, or continue) cannot be used inside an anonymous method to jump outside that anonymous method, and vice versa: a jump statement outside an anonymous method cannot jump inside that anonymous method.

  • Unsafe code cannot be accessed inside an anonymous method. Also, ref and out parameters used outside anonymous methods cannot be accessed. But other variables defined outside the anonymous method can be used.

If you need to write the same function multiple times using anonymous methods, don’t use anonymous methods. At this point, it is better to write a named method than to copy the code, because the method only needs to be written once, and you can refer to it by name later.

Note: The syntax for anonymous methods was introduced in C# 2. In new programs, this syntax is not needed, because lambda expressions provide the same functionality, plus additional functionality. However, in the existing source code, anonymous methods are used in many places, so it is good to know about it. Starting with C# 3.0, lambda expressions are available.

3. Lambda expression

Since C# 3.0, implementation code can be assigned to a delegate using a new syntax: lambda expressions. Lambda expressions can be used wherever there is a delegate parameter type. The previous examples using anonymous methods can be changed to use lambda expressions.

class Program
{<!-- -->
    delegate void MyDelegate(string message);

    static void Main()
    {<!-- -->
        // MyDelegate myDelegate = delegate (string message) { Console. WriteLine("Hello, " + message); };
        MyDelegate myDelegate = message => Console. WriteLine("Hello, " + message);

        myDelegate("world");

        Console. ReadLine();
    }
}

The left side of the lambda operator “=>” lists the required parameters, and its right side defines the implementation code of the method assigned to the lambda variable.

3.1 Lambda expression example

In C#, lambda expressions are a concise syntax for creating anonymous functions. Lambda expressions can be used to define instances of delegates or functional interfaces, making code more concise and readable.

The syntax of a lambda expression is as follows:

(parameters) => expression

where parameters is a comma-separated list of parameters that can contain types or implicit type inference, and expression is the body of the Lambda expression.

Here are some examples of Lambda expressions:

  • Lambda expressions are passed as parameters to methods:
List<int> numbers = new List<int> {<!-- --> 1, 2, 3, 4, 5 };

// pass the lambda expression as a parameter to the Where method
List<int> evenNumbers = numbers.Where(x => x % 2 == 0).ToList();
  • Lambda expressions as instances of delegate types:
// delegate type definition
delegate int CalculateSquare(int x);

// Create a delegate instance using a Lambda expression
CalculateSquare square = x => x * x;

int result = square(5); // prints 25
  • A lambda expression is passed to the sort method as a comparator:
List<string> names = new List<string> {<!-- --> "John", "Alice", "Bob", "David" };

// Use a lambda expression as a comparator passed to the Sort method
names. Sort((a, b) => a. CompareTo(b));

// Output the sorted results: Alice, Bob, David, John
foreach (string name in names)
{<!-- -->
    Console. WriteLine(name);
}

Lambda expressions can capture external variables, which makes them very flexible. Captured variables can be used inside the lambda expression as if they were declared within the scope of the expression.

int multiplier = 2;

// Calculate the product of each element using a Lambda expression
List<int> numbers = new List<int> {<!-- --> 1, 2, 3, 4, 5 };
List<int> multipliedNumbers = numbers. Select(x => x * multiplier). ToList();

// output result: 2, 4, 6, 8, 10
foreach (int number in multipliedNumbers)
{<!-- -->
    Console. WriteLine(number);
}

Lambda expressions are one of the powerful and commonly used features in C#, which simplify many common coding tasks, such as collection filtering, sorting, and transformation. By using Lambda expressions, you can write cleaner, more readable code.

3.2 Parameters

Lambda expressions have several ways of defining parameters. If there is only one parameter, it is sufficient to write only the parameter name. The lambda expression below uses the parameter s. Because the delegate type defines a string parameter, the type of s is string.

Func<string, string> oneParam = s => $"change uppercase {<!-- -->s.ToUpper()}";
Console. WriteLine(OneParam("test"));

If the delegate takes multiple parameters, enclose the parameter names in curly braces. Here the type of parameters x and y is double, defined by Func delegate:

Func<double, double, double> towParams = (x, y) => x * y;
Console.WriteLine(twoParams(3, 2));

For convenience, you can add parameter types to variable names in curly braces. If the compiler can’t match the overloaded version, then using the parameter type can help find a matching delegate:

Func<double, double, double> towParamsWithTypes = (double x, double y) => x * y;
Console.WriteLine(towParamsWithTypes(4, 2));

3.3 Multi-line code

If the lambda expression has only one statement, the curly braces and return statement are not needed inside the method block because the compiler adds an implicit return statement:

Func<double, double> square = x => x * x;

It’s perfectly legal to add curly braces, return statements, and semicolons, and it’s usually easier to read than not adding these symbols:

Func<double, double> square = x =>
{<!-- -->
    return x * x;
}

However, if multiple statements are required in the implementation code of the Lambda expression, curly braces and a return statement must be added:

Func<string, string> lambda = param =>
{<!-- -->
    param += mid;
    param + = " and this was added to the string.";
    return param;
};

3.4 Closure

In C#, a closure (Closure) is a special function object that can capture variables outside its definition scope and use these variables inside the function body. In simple terms, closures allow access to variables outside the function within the function.

The main application scenario of closures in C# is to create anonymous functions through delegation or Lambda expressions, and use external variables inside the function. When a function captures external variables, the lifetime of those variables is extended, and closures can still use those variables even after their scope has ended.

Here is a simple example to illustrate the concept of closures:

using System;

class Program
{<!-- -->
    static Func<int, int> CreateMultiplier(int factor)
    {<!-- -->
        // Create a closure and capture the external variable factor
        return x => x * factor;
    }

    static void Main()
    {<!-- -->
        int multiplier = 5;

        // create closure instance
        var multiplyByFive = CreateMultiplier(multiplier);

        Console.WriteLine(multiplyByFive(3)); // output 15

        multiplier = 10; // Modify the value of the external variable

        Console.WriteLine(multiplyByFive(3)); // output 30, the closure still uses the old variable value
    }
}

In the above example, the CreateMultiplier function creates a closure and captures the external variable factor inside the closure. Then, we call the CreateMultiplier function and pass in a multiplier variable with a value of 5, which returns a function that multiplies the input by 5. We assign this function to the multiplyByFive variable.

Next, we call the multiplyByFive function and pass in 3 as an argument, which will return 15. Then, we modify the outer variable multiplier to 10, but the closure still uses the old variable value, so we call the multiplyByFive function again and pass in 3, which will returns 30.

This example shows how closures can capture and use external variables inside a function, and the closure preserves the state of the captured variables even if those variables have changed since the closure was created.

The use of closures can bring many benefits, such as flexible application in event handlers, LINQ queries, and asynchronous programming. But pay attention to the problem that closures may cause memory leaks, because closures will hold references to external variables, extending the life cycle of these variables. When using closures, you need to pay attention to the life cycle of variables and garbage collection to ensure that resources are not wasted or leaked.

4. Event

In C#, events are a delegation-based mechanism for implementing the publish and subscribe pattern. It allows objects to notify other objects when certain events occur, and other objects can register as listeners of the event to perform corresponding actions when the event occurs.

Events in C# mainly involve three aspects: event publishing, event subscribing and weak events.

  • event release

Event publishing refers to notifying all listeners who subscribe to the event when a specific condition or action occurs. In C#, events are usually defined using the event keyword, which is used to declare an event, and the event is usually represented by a delegate type.

public event EventHandler MyEvent;

The above code defines an event named MyEvent, which is a delegate of type EventHandler. Other objects can receive notifications by subscribing to this event.

Code that triggers events in a class typically follows this pattern:

protected virtual void OnMyEvent(EventArgs e)
{<!-- -->
    MyEvent?.Invoke(this, e);
}

When certain conditions occur, call the OnMyEvent method to trigger an event. This notifies all listeners subscribed to the MyEvent event.

  • event listener

Event monitoring means that an object is registered as a subscriber of an event, so that the corresponding operation is performed when the event occurs. In C#, event handlers can be defined through delegates or Lambda expressions, and then attached to events using the + = operator.

myObject.MyEvent += MyEventHandler;

The above code attaches an event handler named MyEventHandler to the MyEvent event of the myObject object.

The signature of an event handler usually matches the event’s delegate type:

void MyEventHandler(object sender, EventArgs e)
{<!-- -->
    // logic for handling events
}

When an event occurs, all registered event handlers are called in turn.

  • weak event

Weak events are a mechanism to address potential memory leaks between event listeners and event publishers. When an object subscribes to an event, but does not explicitly unsubscribe, the event publisher will keep a reference to the subscriber, making the subscriber unable to be garbage collected.

To avoid this, the weak event pattern can be used. Weak events use weak references (Weak Reference) to store references to event listeners, thereby avoiding explicit references to listeners and allowing the garbage collector to recycle listeners when they are no longer needed.

The WeakEventManager class in .NET Framework provides a general framework to implement the weak event pattern.

Here’s a simple example showing how to use the weak event pattern:

public class EventPublisher
{<!-- -->
    private WeakEventManager _eventManager = new WeakEventManager();

    public event EventHandler MyEvent
    {<!-- -->
        add => _eventManager. AddEventHandler(value);
        remove => _eventManager. RemoveEventHandler(value);
    }

    public void RaiseEvent()
    {<!-- -->
        _eventManager.HandleEvent(this, EventArgs.Empty, nameof(MyEvent));
    }
}

public class EventListener
{<!-- -->
    public EventListener(EventPublisher publisher)
    {<!-- -->
        publisher.MyEvent += HandleEvent;
    }

    private void HandleEvent(object sender, EventArgs e)
    {<!-- -->
        Console. WriteLine("Event handled");
    }
}

public class Program
{<!-- -->
    public static void Main()
    {<!-- -->
        EventPublisher publisher = new EventPublisher();
        EventListener listener = new EventListener(publisher);

        publisher.RaiseEvent(); // output "Event handled"

        // listener can be garbage collected if there are no other references
        listener = null;
        GC. Collect();

        publisher.RaiseEvent(); // listener has been garbage collected and no longer handles events
    }
}

In the above example, EventPublisher is an event publisher and EventListener is an event listener. By using WeakEventManager, listeners that subscribe to MyEvent events will be stored as weak references. When there are no other references to the listener, the garbage collector can reclaim it, thus avoiding memory leaks.

These are the basic concepts and usage of events in C#. Events provide a loosely coupled communication mechanism that enables objects to interact in a flexible manner. Events allow objects to publish messages and have other objects respond to those messages, enabling efficient application design.