Wednesday, October 15, 2008

Callbacks And Delegates

It's been a couple of weeks since last writing. Besides being very busy, I've had a touch of "writer's block". Nothing too serious, I should get over it soon.

Lately, I've been intrigued by the abundant use of callback functions and delegates in the .NET Framework. If you want to do anything interesting, you gotta know callbacks. I'll try to describe callbacks and delegates in simplistic terms. Maybe it will help you over the hump.

Consider the following code...


List list = new List();
//Populate list with some MyClass objects
for(int i=0; i<10; ++i)
list.Add(new MyClass(){ ID=i, strValue=i.ToString() });
//Sort the list
list.Sort();
foreach(MyClass c in list)
Console.WriteLine("{0}",c.strValue);

...

public class MyClass
{
public int ID;
public string strValue;
public override string ToString() { return strValue; }
}


This code throws an exception at the line that says Sort(). That's because the default List.Sort() expects the objects it contains to implement IComparable. Now, we could quickly add IComparable to the class and implement the interface. But, if you don't have control of the class, what would you do then? Or if the built-in IComparable interface didn't sort on the field you want, what then?

You're in luck, because Sort() is overloaded and will accept as a parameter an IComparer or a Comparison object.


public class CompareMyClass : IComparer
{
public int Compare(MyClass x, MyClass y)
{
return x.strValue.CompareTo(y.strValue);
}
}


With this piece of code, you can now call Sort() with a new CompareMyClass object and the exception goes away and the list comes out sorted. This is pretty cool, but it's also pretty static. If I wanted to sort by the ID field instead, I would have to change my class or write a new class. We can reduce some of that by going to the Comparison class. This is a generic class whose constructor takes as an argument a callback function. The Comparison constructor has the calling signature...



Comparison.Comparison(int (T,T) target)

The generic parameter T can be anything. But when we see the signature in the constructor's parameter list, one might start scratching their heads. The best way to read the parameter signature is to start with the word "target". It could have been anything, but "target" suggests that it is the target of some operation. In fact, it is. It is the target function that Sort() will call over and over to determine the correct order of the list elements. The "int (T,T)" is the "type" of the parameter. This means that "target" is a function that takes two parameters of type T and returns an "int" value. Since T is a generic parameter we can replace it with MyClass as we do below. We no longer need the CompareMyClass class, but we do need a function to call.


int MyCompare(MyClass x, MyClass y)
{ return x.strValue.CompareTo(y.strValue); }

// and Sort looks like this...
list.Sort(new Comparison(MyCompare));

You could have two different functions to compare MyClass objects two different ways.



int MyCompare2(MyClass x, MyClass y)
{ return x.ID.CompareTo(y.ID); }

But, then you have to plug in the correct function when you want a different sort behavior. If the comparison type might change at runtime based upon user input, you can set a "delegate" variable and provide that to Comparison() instead.


// at class scope
delegate int MyCompareDelegate(MyClass x, MyClass y); // declares a delegate type

...
// at method scope
MyCompareDelegate dlgt; // declares a delegate variable
if(radioButton1.Checked == true)
dlgt = MyCompare;
else
dlgt = MyCompare2;

list.Sort(new Comparison(dlgt));

The delegate lets us treat the functions as objects. Since a delegate for the callback will work just as well as the callback, we can write the comparison code right at the place where we use it with "anonymous delegates"...


MyCompareDelegate dlgt;
if (radioButton1.Checked == true)
dlgt = delegate(MyClass x, MyClass y)
{
return x.ID.CompareTo(y.ID);
};
else
dlgt = delegate(MyClass x, MyClass y)
{
return x.strValue.CompareTo(y.strValue);
};
//Sort the list
list.Sort(new Comparison(dlgt));

Here we have set the "dlgt" delegate variable to one of two "anonymous" functions. Creating and anonymous function returns a delegate that can be assigned to a variable like any other function, as long as the signatures match. Well if that's the case, then the Lambda syntax should also work, shouldn't it?


MyCompareDelegate dlgt;
if (radioButton1.Checked == true)
dlgt = (x,y) => x.ID.CompareTo(y.ID);
else
dlgt = (x,y) => x.strValue.CompareTo(y.strValue);
//Sort the list
list.Sort(new Comparison(dlgt));

Yes. The Lamda Expression implicitly determines the types of x, y and the return value from the delegate assignment and context. Pretty cool!

I stop there. This is starting to be too much fun. The power of the delegate and callback is huge. Keep playing with these and you'll get the hang of it.

No comments: