[Journal - More Fun With Callbacks]

More Fun With Callbacks

Thursday, January 12, 2006

A common pattern in programming is using callbacks. Callbacks come in various forms, such as function pointers, delegates, overridden virtual methods, interface implementations, or event handlers. What's common is that a general algorithm exists, with the callback providing some form of behavioural detail.

In .NET, the most convenient way to customize code with callbacks is using delegates. Before Whidbey, this meant two things:

  1. Defining a suitable delegate type.
  2. Defining a callback method.
delegate bool StringFilter(string s);

class Test {

    public static void Foo(){
        PrintStrings(
            new StringFilter(IsNonEmpty),
            "foo", string.Empty, "bar"
        );
    }

    private static void PrintStrings(StringFilter filter,
                                     params string[] strings){
        foreach(string s in strings){
            if(filter(s)){
                Console.WriteLine(s);
            }
        }
    }

    private static bool IsNonEmpty(string s){
        return (s.Length > 0);
    }

}

Now, separating out the filter method can have advantages, but it's not always needed - sometimes, the callback is just too specialized, if that's so, you might be better off if it's as inaccessible as possible instead of over-generalizing the specific. In other words, a nested function, aka, anonymous method is what you want.

The same considerations may apply to defining a delegate type. The solution from Whidbey onwards, is to have a few generic delegate definitions that are overloaded by their generic type parameters. An example of that would be the System.Query.Func delegate types envisioned for Orcas. Here's my version of it:

public delegate TR Callback<TR>();
public delegate TR Callback<TR, T0>(T0 arg0);
public delegate TR Callback<TR, T0, T1>(T0 arg0, T1 arg1);
// and a few more
public delegate void VoidCallback();
public delegate void VoidCallback<T0>(T0 arg0);
public delegate void VoidCallback<T0, T1>(T0 arg0, T1 arg1);
// and so on

In contrast to System.Query.Func, I've decided to put the return type first, which is more in sync with C# syntax, and makes the overload list look more elegant (for what it's worth). Another tidbit: since void is not a type really and thus can't be a generic type argument, there have to be special delegate types for this case.

Anyway, here's the Whidbey version:

class Test {

    public static void Foo(){
        PrintStrings(
            delegate(string s){
                return (s.Length > 0);
            },
            "foo", string.Empty, "bar"
        );
    }

    private static void PrintStrings(Callback<bool, string> filter,
                                     params string[] strings){
        foreach(string s in strings){
            if(filter(s)){
                Console.WriteLine(s);
            }
        }
    }

}

The nice thing here is that all locals in the enclosing method are accessible for both reading and writing, and, if the enclosing method is an instance method, the this reference naturally refers the enclosing instance.

While overloaded delegate types are convenient, there are still cases where defining a specific type is preferable for reasons of clarity - a good type name is sure worth something!

In Orcas (.NET 3.0), the syntax will be even shorter with lambda expressions.