Introduction

This article provides a comprehensive understanding of delegates, lambda expressions, and how to use delegates with lambda expressions in C#. We'll explore the evolution of delegates from C# 1.0 through to modern lambda expressions.

What is a Delegate?

Delegates are classes that hold function references for .NET (specifically, the Common Language Infrastructure). They are similar to C++ function pointers but with a crucial difference:

Key Difference:
  • C++ Function Pointers: Only hold memory location of a function
  • C# Delegates: Type-safe classes that define return type and parameter types

Delegates allow you to pass methods as parameters to other methods, enabling powerful callback mechanisms and event-driven programming.

Declaring a Delegate

Syntax for declaring a delegate:

<Access Modifier> delegate <Return Type> <Delegate Name>(<Parameters>)

// Example
public delegate int Del(int Value);

Initializing and Invoking Delegates

public delegate int Del(int Value);

public static int AddFive(int Value)
{
    return Value + 5;
}

// Initialize delegate
Del obj = new Del(AddFive);

// Invoke delegate
Console.WriteLine(obj(50)); // Output: 55

// Or use Invoke method
Console.WriteLine(obj.Invoke(50)); // Output: 55

Understanding Delegates Internally

When you declare a delegate, the C# compiler generates a sealed class derived from System.MulticastDelegate, which in turn derives from System.Delegate:

public delegate int fun(int Value);

fun ok = new fun(del);
Type type = ok.GetType();

Console.WriteLine("Base Class: " + type.BaseType);     // System.MulticastDelegate
Console.WriteLine("Is Class: " + type.IsClass);        // True
Console.WriteLine("Is Sealed: " + type.IsSealed);      // True

Types of Delegates

1. Singlecast Delegate

A Singlecast delegate (derived from System.Delegate) contains a reference for one method at a time:

public delegate void Del();

public void DisplayMessage()
{
    Console.WriteLine("Hello from DisplayMessage");
}

// Usage
Del obj = new Del(DisplayMessage);
obj.Invoke();

2. Multicast Delegate

A Multicast delegate (derived from System.MulticastDelegate) can contain references for multiple methods. Think of it like a coffee vending machine that combines ingredients:

public delegate void Vender();

public static void Milk()
{
    Console.Write("Milk + ");
}

public static void Tea()
{
    Console.Write("Tea = Tea");
}

public static void Coffee()
{
    Console.Write("Coffee = Coffee");
}

// Usage
Vender vender = new Vender(Milk);
int option = 1; // 1 for Tea, 2 for Coffee

switch (option)
{
    case 1:
        vender += Tea;    // Add Tea to the delegate
        break;
    case 2:
        vender += Coffee; // Add Coffee to the delegate
        break;
}

vender(); // Invokes: Milk + Tea = Tea

Multicast Delegate Operators

  • + or += - Add function reference
  • - or -= - Remove function reference

Action<T> and Func<T> Delegates

Instead of creating custom delegates, you can use predefined Action<T> and Func<T> delegates from the System namespace:

// Action<T> - Returns void, can have 0-16 parameters
Action<string> printMessage = (msg) => Console.WriteLine(msg);
printMessage("Hello World");

// Func<T> - Returns a value, can have 0-16 parameters
// Last type parameter is the return type
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(5, 3)); // Output: 8

Delegate Evolution

Delegates have evolved significantly across C# versions:

  • C# 1.0: Create delegate instance with method reference
  • C# 2.0: Anonymous methods introduced
  • C# 3.0: Lambda expressions (more expressive and concise)

Anonymous Methods (C# 2.0)

Anonymous methods allow you to define inline blocks of code as delegate parameters, reducing the need for separate method declarations:

public delegate void Del();

// Without parameters
Del obj = delegate() {
    Console.WriteLine("Anonymous method without parameters");
};
obj();

// With parameters
public delegate void Del(string message);

Del obj2 = delegate(string message) {
    Console.WriteLine(message);
};
obj2("Hello from anonymous method");
⚠︎ Important: You cannot use goto, break, or continue statements inside anonymous methods if the target is outside the block.

Lambda Expressions (C# 3.0)

Lambda expressions are anonymous functions that provide a more concise syntax than anonymous methods. They are especially useful for writing LINQ queries.

Lambda Expression Syntax

(parameters) => { expression or statement; }

// Example with explicit return
Del obj = (Value) => {
    int x = Value * 2;
    return x;
};

// Simplified version (single expression)
Del obj = Value => Value * 2;

Lambda Expression Rules

  • Parentheses are optional for single parameter
  • Curly braces are optional for single statement
  • Return keyword is optional for single expression
  • Type inference determines parameter types automatically

Examples of Lambda Expressions

// No parameters
() => Console.WriteLine("No parameters");

// Single parameter (parentheses optional)
x => x * x

// Multiple parameters (parentheses required)
(x, y) => x + y

// Explicit type declaration
(int x, int y) => x + y

// Multiple statements (curly braces required)
(x, y) => {
    int sum = x + y;
    return sum * 2;
}

Lambda with Different Delegate Types

// With custom delegate
public delegate int Del(int Value);
Del obj = Value => Value * 2;
Console.WriteLine(obj(5)); // Output: 10

// With Func<T>
Func<int, int> square = x => x * x;
Console.WriteLine(square(5)); // Output: 25

// With Action<T>
Action<string> print = msg => Console.WriteLine(msg);
print("Hello Lambda"); // Output: Hello Lambda

// With Predicate<T>
Predicate<int> isPositive = x => x > 0;
Console.WriteLine(isPositive(5));  // Output: True
Console.WriteLine(isPositive(-3)); // Output: False

Variable Scope with Lambda Expressions

Lambda expressions can access variables declared outside their block. The compiler creates an anonymous class to manage these captured variables:

delegate bool Del(int z);

public void Init(int Value)
{
    int j = 0;
    Del del = (x) => { 
        j = 12; 
        return x == j; 
    };
    
    Console.WriteLine("j before: " + j); // Output: 0
    bool result = del(12);
    Console.WriteLine("j after: " + j);  // Output: 12
    Console.WriteLine("result: " + result); // Output: True
}
⚠︎ Important: Captured variables will not be garbage-collected until the delegate that references them goes out of scope. This can lead to unexpected behavior if not handled carefully.

Practical Examples

Sorting with Lambda Expressions

List<int> numbers = new List<int> { 5, 2, 8, 1, 9, 3 };

// Sort ascending
numbers.Sort((a, b) => a.CompareTo(b));

// Sort descending
numbers.Sort((a, b) => b.CompareTo(a));

// Custom object sorting
List<Person> people = new List<Person>
{
    new Person { Name = "Alice", Age = 30 },
    new Person { Name = "Bob", Age = 25 },
    new Person { Name = "Charlie", Age = 35 }
};

// Sort by age
people.Sort((p1, p2) => p1.Age.CompareTo(p2.Age));

// Sort by name
people.Sort((p1, p2) => p1.Name.CompareTo(p2.Name));

Event Handling with Lambda Expressions

// Button click event (in WPF/WinForms)
button.Click += (sender, e) => {
    Console.WriteLine("Button clicked!");
    // Handle click logic here
};

// Timer elapsed event
timer.Elapsed += (sender, e) => {
    Console.WriteLine("Timer elapsed at " + DateTime.Now);
};

LINQ Queries with Lambda Expressions

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Filter even numbers
var evenNumbers = numbers.Where(n => n % 2 == 0);

// Transform data
var squared = numbers.Select(n => n * n);

// Find first element matching condition
var firstGreaterThanFive = numbers.FirstOrDefault(n => n > 5);

// Check if any element matches condition
bool hasEvenNumber = numbers.Any(n => n % 2 == 0);

// Get sum of elements matching condition
int sumOfEvens = numbers.Where(n => n % 2 == 0).Sum();

Best Practices

Tips for Using Delegates and Lambda Expressions

  • Prefer lambda expressions over anonymous methods for cleaner code
  • Use Action<T> and Func<T> instead of custom delegates when possible
  • Keep lambda expressions short and simple (ideally one line)
  • Be cautious with captured variables - they can cause memory leaks
  • Use explicit parameter types when type inference fails
  • Consider extracting complex lambda expressions into named methods

Common Pitfalls

⚠︎ Watch Out For:
  • Closure Variables: Variables captured in loops can lead to unexpected behavior
  • Memory Leaks: Long-lived delegates holding references to large objects
  • Performance: Creating many delegate instances can impact performance
  • Debugging: Lambda expressions can be harder to debug than named methods

Conclusion

Delegates, anonymous methods, and lambda expressions are powerful features in C# that enable functional programming patterns, event-driven architectures, and clean, expressive code. Understanding their evolution from C# 1.0 through modern versions helps developers write more maintainable and efficient applications. Lambda expressions, in particular, are essential for LINQ queries and modern C# development.

While delegates provide type-safe function pointers, and anonymous methods offer inline code blocks, lambda expressions combine the best of both worlds with concise syntax and powerful capabilities. Use them wisely to create elegant solutions to complex problems.