How to Pass Delegates for Callbacks in C

In C#, passing delegates as method parameters is a standard way to implement the callback pattern, allowing a method to notify callers or execute custom logic upon completing a task. This article provides a straightforward guide on how to implement callbacks using both standard built-in delegates like Action and Func, as well as custom-defined delegates, along with best practices for safe execution using null-conditional operators and lambda expressions.

Understanding Delegates as Callbacks

A delegate is a type-safe function pointer that defines a method signature. When you pass a delegate to a method, you are passing executable code that the receiving method can call at an appropriate time. This decouples the execution logic from the caller's reaction to the result.

Using Built-In Delegates: Action and Func

In modern C#, you rarely need to declare custom delegate types. The framework provides built-in generic delegates:

  • Action: Used for callbacks that do not return a value (void).
  • Func: Used for callbacks that return a value.

Implementing a Void Callback with Action

Use Action<T> when the calling method needs to send data back to the caller without expecting a return value.

public class DataProcessor
{
    // The method accepts an Action delegate as a callback parameter
    public void ProcessData(int data, Action<string> onCompleted)
    {
        // Simulate work
        string result = $"Processed data: {data * 2}";

        // Invoke the callback safely using the null-conditional operator
        onCompleted?.Invoke(result);
    }
}

You can call this method by passing an existing method or an inline lambda expression:

var processor = new DataProcessor();

// Passing an inline lambda expression as the callback
processor.ProcessData(10, (message) => 
{
    Console.WriteLine($"Callback received: {message}");
});

Implementing a Callback with a Return Value using Func

Use Func<T, TResult> when the processing method requires the callback to perform an evaluation and return a result back to the caller.

public class FilterService
{
    // The method takes a Func delegate that receives a string and returns a boolean
    public void FilterNames(List<string> names, Func<string, bool> predicate)
    {
        foreach (var name in names)
        {
            if (predicate != null && predicate(name))
            {
                Console.WriteLine($"Matched: {name}");
            }
        }
    }
}

Call the method using a lambda expression:

var service = new FilterService();
var names = new List<string> { "Alice", "Bob", "Charlie" };

// Pass a delegate that checks if the name starts with 'A'
service.FilterNames(names, name => name.StartsWith("A"));

Using Custom Delegates

If you require explicit parameter names for clarity or need to support legacy codebases, you can declare a custom delegate type using the delegate keyword.

// 1. Declare the delegate signature
public delegate void ProgressCallback(int percentage, string statusMessage);

public class Downloader
{
    // 2. Accept the custom delegate as a parameter
    public void DownloadFile(string url, ProgressCallback progress)
    {
        for (int i = 1; i <= 100; i += 50)
        {
            // 3. Invoke the delegate
            progress?.Invoke(i, "Downloading...");
        }
        progress?.Invoke(100, "Completed");
    }
}

Usage:

var downloader = new Downloader();

downloader.DownloadFile("https://example.com/file.zip", (percent, status) =>
{
    Console.WriteLine($"[{percent}%] {status}");
});

Best Practices

  • Check for Null: Always use the null-conditional invocation pattern (callback?.Invoke(...)) to prevent NullReferenceException if the caller passes null.
  • Prefer Action and Func: Avoid creating custom delegates unless the signature requires specific semantic parameter names that cannot be understood via generic types.
  • Async Callbacks: When passing callbacks to asynchronous methods, accept Func<Task> instead of Action so the receiving method can await the completion of the callback.