Passing Callback Functions in C#: Implementing Cross-Class Method Invocation with Action<T> Delegates

Dec 02, 2025 · Programming · 12 views · 7.8

Keywords: C# Callback Functions | Action Delegates | Cross-Class Method Invocation

Abstract: This article provides an in-depth exploration of how to pass callback functions to another class and execute them at appropriate times in C#. By analyzing a common cross-class callback scenario, it explains why using Action<string> delegates is safer and more type-safe than the raw Delegate type. Starting from the problem context, the article progressively demonstrates code refactoring, compares Objective-C and C# implementation approaches, and offers complete code examples with best practice recommendations.

Problem Context and Scenario Analysis

In object-oriented programming, it is often necessary to pass a method from one class as a callback function to another class for execution when specific events occur. This pattern is particularly common in asynchronous operations, event handling, and cross-module communication. The question describes a typical scenario: in Class1, when a button click event is triggered, a ServerRequest object needs to be created to perform a server request, with myCallback invoked upon completion to process the response.

The original code attempts to use the Delegate type for passing the callback function:

public class Class1{
    private void btn_click(object sender, EventArgs e)
    {
        ServerRequest sr = new ServerRequest();
        sr.DoRequest("myrequest", myCallback);
    }

    public void myCallback(string str)
    {
        // Handle callback logic
    }
}

public class ServerRequest
{
    public void DoRequest(string request, Delegate callback)
    {
        // Perform server request...
        callback("asdf");
    }
}

While this approach compiles, it is prone to type conversion exceptions at runtime because Delegate is a generic base class lacking specific type information. The questioner, coming from an Objective-C background, is accustomed to dynamic method invocation via performSelector:, but C# requires a more type-safe solution.

Solution: Using Action<T> Delegates

C# provides the Action<T> delegate type specifically for representing methods that take parameters but return no value. For callback functions accepting a single string parameter and returning void, Action<string> is the most appropriate choice. The refactored code is as follows:

public class Class1{
    private void btn_click(object sender, EventArgs e)
    {
        ServerRequest sr = new ServerRequest();
        sr.DoRequest("myrequest", myCallback);
    }

    public void myCallback(string str)
    {
        Console.WriteLine("Callback executed with parameter: " + str);
    }
}

public class ServerRequest
{
    public void DoRequest(string request, Action<string> callback)
    {
        // Simulate server request processing
        string response = "Request processed";
        
        // Execute the callback function
        if (callback != null)
        {
            callback(response);
        }
    }
}

This implementation offers several advantages:

In-Depth Technical Analysis

Action<T> is a predefined generic delegate type in the .NET Framework, part of the System namespace. Its definition is:

public delegate void Action<in T>(T obj);

The in keyword denotes contravariance, allowing the use of derived types as parameters and enhancing type flexibility. For callback functions requiring multiple parameters, overloaded versions like Action<T1, T2> are available, supporting up to 16 parameters.

Compared to Objective-C's performSelector:, C#'s delegate mechanism provides stronger type checking and compile-time validation. While Objective-C's dynamic nature offers flexibility, it is susceptible to unrecognized selector errors at runtime. C#'s static type system ensures method correctness at compile time through delegates.

Practical Applications and Extensions

In real-world development, callback functions are frequently used in asynchronous programming patterns. Combined with the async/await keywords, more modern asynchronous code can be created:

public async Task DoRequestAsync(string request, Action<string> callback)
{
    // Asynchronously execute server request
    string response = await FetchDataFromServerAsync(request);
    
    // Execute callback on the main thread (if needed)
    await Dispatcher.CurrentDispatcher.InvokeAsync(() =>
    {
        callback?.Invoke(response);
    });
}

For scenarios requiring return values, the Func<T, TResult> delegate can be used. For example, if the callback function needs to return a processing result:

public void ProcessRequest(string request, Func<string, bool> callback)
{
    string data = GetRequestData(request);
    bool result = callback(data);
    // Perform subsequent processing based on the result
}

Best Practices and Considerations

  1. Null Checking: Always verify callback != null before invocation to avoid NullReferenceException.
  2. Exception Handling: Consider wrapping callback execution in try-catch blocks to prevent exceptions in callbacks from affecting the main flow.
  3. Thread Safety: If callbacks may execute on different threads, ensure appropriate synchronization mechanisms.
  4. Delegate Chaining: Multiple callback methods can be combined using the += operator to form multicast delegates.

By appropriately utilizing Action<T> delegates, developers can build type-safe, maintainable callback mechanisms, a fundamental and essential skill in modern C# application development.

Copyright Notice: All rights in this article are reserved by the operators of DevGex. Reasonable sharing and citation are welcome; any reproduction, excerpting, or re-publication without prior permission is prohibited.