C# Delegates

Passing methods as parameters for flexible code

🎯 What are Delegates?

Delegates are type-safe function pointers that reference methods. They allow you to pass methods as parameters, enabling callbacks and event handling in your applications with flexibility and safety.


// Define a delegate
delegate void MessageDelegate(string message);

// Method that matches delegate signature
void ShowMessage(string msg)
{
    Console.WriteLine("Message: " + msg);
}

// Use the delegate
MessageDelegate del = ShowMessage;
del("Hello!"); // Calls ShowMessage
                                    

Output:

Message: Hello!

Delegate Concepts

📌

Basic Delegates

Reference to a method

delegate void MyDelegate
(string s);
🔗

Multicast

Call multiple methods at once

del += Method1;
del += Method2;
âš¡

Action & Func

Built-in delegate types

Action<string> action;
Func<int, bool> func;
🎨

Lambda

Inline anonymous methods

del = (x) => 
Console.WriteLine(x);

🔹 Basic Delegate Usage

A delegate is a type-safe function pointer that defines a method signature and can reference any matching method. It allows methods to be passed as parameters, stored in variables, and invoked dynamically. This enables callback mechanisms, event handling, and flexible algorithm design (like passing comparison logic to a sort method). Delegates are the foundation for events and LINQ in C#, providing the ability to treat code as data and implement powerful patterns like strategy or observer.

using System;

// Define a delegate type
delegate void PrintDelegate(string message);

class Program
{
    // Methods that match the delegate signature
    static void PrintToConsole(string msg)
    {
        Console.WriteLine("Console: " + msg);
    }
    
    static void PrintWithStars(string msg)
    {
        Console.WriteLine("*** " + msg + " ***");
    }
    
    static void Main()
    {
        // Create delegate instances
        PrintDelegate printer;
        
        // Point to first method
        printer = PrintToConsole;
        printer("Hello World");
        
        // Point to second method
        printer = PrintWithStars;
        printer("Hello World");
        
        // Pass delegate as parameter
        ExecutePrint(PrintToConsole, "Passed as parameter");
    }
    
    static void ExecutePrint(PrintDelegate del, string message)
    {
        del(message);
    }
}

Output:

Console: Hello World

*** Hello World ***

Console: Passed as parameter

🔹 Multicast Delegates

Multicast delegates in C# allow a single delegate instance to reference multiple methods. When the delegate is invoked, all attached methods are called in sequence, making them ideal for implementing event handling, logging systems, or notification chains. For instance, a system update event might simultaneously trigger a log entry, send an email, and remove an old SMS alert. Use the `+=` operator to add methods and `-=` to remove them, ensuring flexible and maintainable callback management within your applications.

using System;

delegate void NotifyDelegate(string message);

class Program
{
    static void SendEmail(string msg)
    {
        Console.WriteLine("Email sent: " + msg);
    }
    
    static void SendSMS(string msg)
    {
        Console.WriteLine("SMS sent: " + msg);
    }
    
    static void LogMessage(string msg)
    {
        Console.WriteLine("Logged: " + msg);
    }
    
    static void Main()
    {
        // Create multicast delegate
        NotifyDelegate notify = SendEmail;
        notify += SendSMS;      // Add second method
        notify += LogMessage;   // Add third method
        
        Console.WriteLine("Sending notification...\n");
        notify("System update available");
        
        // Remove a method
        Console.WriteLine("\nRemoving SMS notification...\n");
        notify -= SendSMS;
        notify("New message");
    }
}

Output:

Sending notification...

Email sent: System update available

SMS sent: System update available

Logged: System update available

Removing SMS notification...

Email sent: New message

Logged: New message

🔹 Action and Func Delegates

The Action and Func built-in delegate types work seamlessly with lambda expressions for different purposes. Action represents a void method, used for side effects like printing: Action<string> greet = name => Console.WriteLine($"Hello, {name}!"); outputs "Hello, Alice!". Func represents a method returning a value, used for computations: Func<int, int, int> add = (a, b) => a + b; returns 20 for 10+10. They encapsulate behavior inline, promoting functional programming patterns in C#.

using System;

class Program
{
    static void Main()
    {
        // Action - no return value
        Action<string> greet = (name) => 
        {
            Console.WriteLine("Hello, " + name + "!");
        };
        greet("Alice");
        
        // Action with multiple parameters
        Action<string, int> displayInfo = (name, age) =>
        {
            Console.WriteLine($"{name} is {age} years old");
        };
        displayInfo("Bob", 30);
        
        // Func - returns a value
        Func<int, int, int> add = (a, b) => a + b;
        int sum = add(5, 3);
        Console.WriteLine("Sum: " + sum);
        
        // Func with complex logic
        Func<string, bool> isLongName = (name) =>
        {
            return name.Length > 5;
        };
        Console.WriteLine("Is 'Alexander' long? " + isLongName("Alexander"));
        Console.WriteLine("Is 'Bob' long? " + isLongName("Bob"));
    }
}

Output:

Hello, Alice!

Bob is 30 years old

Sum: 8

Is 'Alexander' long? True

Is 'Bob' long? False

🔹 Lambda Expressions with Delegates

Lambda expressions offer a concise, inline way to define anonymous methods for delegates. Written as `(parameters) => expression`, they simplify operations like filtering lists or transforming data without declaring full methods. For example, `numbers.Where(n => n % 2 == 0)` quickly extracts even numbers. Lambdas are especially useful with `Action` and `Func` delegates for short, one-off operations, enhancing code readability and reducing boilerplate in scenarios such as event handlers or LINQ queries.

using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        
        // Lambda with Func delegate
        Func<int, bool> isEven = x => x % 2 == 0;
        var evenNumbers = numbers.Where(isEven);
        Console.WriteLine("Even numbers: " + string.Join(", ", evenNumbers));
        
        // Inline lambda
        var oddNumbers = numbers.Where(x => x % 2 != 0);
        Console.WriteLine("Odd numbers: " + string.Join(", ", oddNumbers));
        
        // Lambda with multiple statements
        Action<int> processNumber = (n) =>
        {
            int square = n * n;
            Console.WriteLine($"{n} squared is {square}");
        };
        
        Console.WriteLine("\nProcessing numbers:");
        processNumber(3);
        processNumber(5);
        
        // Lambda for transformation
        Func<string, string> formatName = name => 
            $"Mr./Ms. {name.ToUpper()}";
        
        Console.WriteLine("\n" + formatName("alice"));
        Console.WriteLine(formatName("bob"));
    }
}

Output:

Even numbers: 2, 4, 6, 8, 10

Odd numbers: 1, 3, 5, 7, 9

Processing numbers:

3 squared is 9

5 squared is 25

Mr./Ms. ALICE

Mr./Ms. BOB

🔹 Delegates as Callbacks

Delegates are commonly used to implement callback patterns, enabling methods to notify callers upon completion. By passing a delegate as a parameter, an asynchronous operation can signal when it finishes, passing back results or status. This pattern is foundational for async programming and event-driven architectures. For instance, a data processor might accept an `Action` callback to log "Processing complete!" with a computed sum, facilitating decoupled, responsive application design.

using System;

class DataProcessor
{
    // Method that accepts a callback delegate
    public void ProcessData(int[] data, Action<string> callback)
    {
        Console.WriteLine("Processing data...");
        
        int sum = 0;
        foreach (int num in data)
        {
            sum += num;
        }
        
        // Call the callback when done
        callback($"Processing complete! Sum = {sum}");
    }
}

class Program
{
    static void Main()
    {
        DataProcessor processor = new DataProcessor();
        int[] numbers = { 1, 2, 3, 4, 5 };
        
        // Pass callback as lambda
        processor.ProcessData(numbers, (result) =>
        {
            Console.WriteLine("Callback received: " + result);
        });
        
        // Pass callback as method
        processor.ProcessData(numbers, OnProcessComplete);
    }
    
    static void OnProcessComplete(string message)
    {
        Console.WriteLine("Method callback: " + message);
    }
}

Output:

Processing data...

Callback received: Processing complete! Sum = 15

Processing data...

Method callback: Processing complete! Sum = 15

🔹 Delegate Types Summary

Understanding when to use each delegate type:

Delegate Types:

  • Custom Delegate: When you need a specific, reusable signature
  • Action<T>: Methods with parameters but no return value (up to 16 parameters)
  • Func<T, TResult>: Methods that return a value (last type parameter is return type)
  • Predicate<T>: Methods that return bool (used for testing conditions)
  • Lambda Expressions: Short, inline anonymous methods
// Examples of each type
delegate int CustomDelegate(int x, int y);

Action<string> action = s => Console.WriteLine(s);

Func<int, int, int> func = (a, b) => a + b;

Predicate<int> predicate = x => x > 0;

🧠 Test Your Knowledge

Which built-in delegate type is used for methods that return a value?