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 preventNullReferenceExceptionif the caller passesnull. - Prefer
ActionandFunc: 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 ofActionso the receiving method canawaitthe completion of the callback.