Showing posts with label Delegates. Show all posts
Showing posts with label Delegates. Show all posts

Tuesday, March 27, 2012

Introduction to Covariance and Contravariance in C#

Covariance and contravariance have had a tweak in C# 4.0. But before we can start using the benefits in our code, we need to know exactly what they are! The subject can be a little bit tricky when you first encounter it, so in this post I'm going to introduce it in a way that I hope is easy to follow.

Almost all of the information in this post comes from Eric Lippert's excellent series of posts, and I recommend you go and take a look at his blog right now (links to the whole series are at the bottom of this post.)

Tigers and a giraffe
Tigers and a giraffe (credit: andrewmalone)

If you 'get it' straight away, great - stick with that, he's the expert. If you'd like a slightly gentler introduction, avoiding (I hope!) common 'gotchas' for learning the subject, read my post (this post) first.

What I have done is to explain the same concepts using the same examples, but in an order and with an emphasis which I think make it much easier to understand the basics. This is particularly good for you if you are a programmer coming at it from the cold, i.e. you haven't encountered covariance and contravariance before, or you have but you don't understand them yet.

When you're done here I'd suggest you go back and read Eric's posts in order - they should be much easier for you to read by then. Eric's posts will flesh out all the interesting details, and continue on to discuss more advanced topics.

Inheritance and assignability
We're not going to begin talking about covariance and contravariance straight away. First, we're going to make a distinction between inheritance and assignability.

As Eric points out, for any two types T and U, exactly one of the following statements is true:

  • T is bigger than U
  • T is smaller than U
  • T is equal to U
  • T is not related to U

Now, one of the things that seems to have caused some confusion on Eric's blog (see the comments) is usage of the phrase "is smaller than". It is used frequently, and is key, so I want to make it's definition crystal clear now before we move on. Eric says:

"Suppose you have a variable, that is, a storage location. Storage locations in C# all have a type associated with them. At runtime you can store an object which is an instance of an equal or smaller type in that storage location."

In simple scenarios, this is something so familiar to programmers that it's barely worth mentioning. We all know that, looking at the list above, only in the middle two scenarios is T assignable to U. The smaller than relation:

class U{}
class T : U{}
U myInstance = new T();

This is the first thing that comes to mind, right? An inheritance hierarchy.

But Eric didn't mention inheritance hierarchies. Sure, an inheritance hierarchy is one way to make a T which is assignable to a U, but what about this one:

U[] myArray = new T[10];

... or, the same statement using classes from the animals hierarchy:

Animal[] animals = new Giraffe[10];

The type Animal inherits from Giraffe, but the type Animal[] doesn't inherit from Giraffe[]. They are assignable, but not linked by inheritance, and this tells us something about what 'is smaller than' means:

  • T is smaller than U

can be read as

  • T is assignable to U

You can visualise it this way:

T is smaller than U
T        <        U
        -->          //Direction of assignability

As we have seen, in some cases this direction of assignability may be because of an inheritance relationship, but in others it is simply because the CLR and languages (C#, Java etc.) happen to support that particular assignment operation.

There is still an inheritance hierarchy involved, i.e. this wouldn't work:

Tiger[] tigers = new Giraffe[10]; //illegal

But the key thing is that there is a difference between inheritance and assignability: they are not the same thing.

I'll say it one more time (for good luck!): The phrase "is smaller than" refers to assignability, not inheritance. The direction of assignability always flows from the smaller type to the larger type. We'll come back to this in a moment.

Covariance and Contravariance
Eric's second post discusses the array assignment operation (the one I used in the Animal/Giraffe example above), and the problems with it. It's definitely worth reading, but park it for now, because things really come alive in post number three.

Eric's example uses delegate methods, and I'll use a simplified version of it here, just to get us started.

It is clear why this is a legal operation:

static Giraffe MakeGiraffe()
{
    return new Giraffe();
}

//Inside some method:
Func<Animal> func = MakeGiraffe;
//             <-- Direction of assignment

Notice that in the assignment operation, Animal is on the left and Giraffe is on the right. That is, the declared type is based on Animal and the assigned type is based on Giraffe.

Now let's look at another example:

static void AcceptAnimal(Animal animal)
{
    //operate on animal
}

//Inside some method:
Action<Giraffe> action = AcceptAnimal;
//             <-- Direction of assignment

Notice that Giraffe is on the left and Animal is on the right. That is, the declared type is based on Giraffe and the assigned type is based on Animal.

The Func<out T> assignment operation supports covariance. The Action<in T> assignment operation supports contravariance.

What does that mean?
Have a quick look at this summary:
(remember to read < as 'is smaller than' and 'is assignable to')

//Direction of assignability -->
Giraffe                       <   Animal

Giraffe MakeGiraffe()         <   Func<Animal>    //covariance
AcceptAnimal(Animal animal)   <   Action<Giraffe> //contravar..

Now read Eric's definition of covariance and contravariance, from the first post in his series:
(the "operation" which manipulate types being the two assignment operations)

Consider an "operation" which manipulates types. If the results of the operation applied to any T and U always results in two types T' and U' with the same relationship as T and U, then the operation is said to be "covariant". If the operation reverses bigness and smallness on its results but keeps equality and unrelatedness the same then the operation is said to be "contravariant".

Hopefully it should start to become clear. In line 4 above, the direction of assignability with respect to the original types, was preserved, while in line 5 it was reversed!

Line 4 represents a covariant operation, and line 5 represents a contravariant operation.

The main heuristic
Let's put it back to C# code so that we can see it with the right-to-left assignability we are used to (now the smaller types are on the right):

Animal animal = new Giraffe(); //basic type assignment

Func<Animal> func = MakeGiraffe;       //covariant
Action<Giraffe> action = AcceptAnimal; //contravariant
// <-- Direction of assignability

Notice how in the covariant operation, Animal and Giraffe are on the same sides as in the basic type assignment operation. And notice how in the contravariant operation, they are on opposite sides - the operation "reverses bigness and smallness".

In both cases, the opposites are illegal. As Eric puts it in post number five:

"Stuff going 'in' may be contravariant,
stuff going 'out' may be covariant"

... but not vice-versa:

Func<Giraffe> func = MakeAnimal;       //contravariant (illegal)
Action<Animal> action = AcceptGiraffe; //covariant (illegal)
// <-- Direction of assignability

And by the way, if there's one heuristic you remember as a result of reading this post, it's probably best to make it the one above!

I'll repeat it later in this article.

Hang on, methods aren't types!
A quick aside - at this stage you might be asking why I'm referring to methods as though they were types. The straight answer is, I'm copying Eric. His caveat:

"A note to nitpickers out there: yes, I said earlier that variance was a property of operations on types, and here I have an operation on method groups, which are typeless expressions in C#. I’m writing a blog, not a dissertation; deal with it!"

Can't argue with that.

What's new in C# 4.0?
Well, 'new' is the wrong word since the stable release of C# 4.0 was two years ago! But all of the types of variance we've looked at so far in this post have been supported since C#2 or before.

We as developers didn't really have to think about those types of variance to use them, because it wasn't exposed syntactically. In other words, we didn't have to write anything different to make it happen, it's just what is and what isn't supported by C# compilers and the CLR.

In post numbers four and six, Eric discusses types of variance which went on to become part of the specification for C# 4.0, and it's those types of variance that I'll discuss now.

Real delegate variance
The first one is easy, and it's discussed in post number four. It's simply about taking the operations which were already legal in terms of method groups and making the same operations legal in terms of typed expressions.

Take our covariant example from earlier:

static Giraffe MakeGiraffe()
{
    return new Giraffe();
}

//Inside some method:
Func<Animal> func = MakeGiraffe;
//             <-- Direction of assignment

Well, in C#3 this essentially equivalent operation was illegal, whereas in C#4 it is legal:

Func<Animal> func = new Func<Giraffe>(() => new Giraffe());
//             <-- Direction of assignment

In fact because of lambda syntax and inferred typing, it can be shortened to:

Func<Animal> func = () => new Giraffe();
//             <-- Direction of assignment

You can now do with typed expressions what you could already do with method groups. Simple.

But here's where it makes sense to quickly explain something I breezed over earlier.

Covariance and Contravariance, at once
Take a look again at the heuristic:

"Stuff going 'in' may be contravariant,
stuff going 'out' may be covariant"

So what happens when you are dealing with a type which has both an 'in' and an 'out'?

The short answer is: it can be covariant, contravariant, both, or neither. But it's easier than that makes it sound!

Take a look at this example. It's a Func that accepts a Mammal and returns a Mammal:

Func<Mammal, Mammal> func;

Now here are some assignment operations:

  • This is a covariant operation:

Func<Mammal, Giraffe> toAssign = //somehow initialise;
Func<Mammal, Mammal> func = toAssign;

  • This is a contravariant operation:

Func<Animal, Mammal> toAssign = //somehow initialise;
Func<Mammal, Mammal> func = toAssign;

  • This is both:

Func<Animal, Giraffe> toAssign = //somehow initialise;
Func<Mammal, Mammal> func = toAssign;

... and, well, I'm sure I don't need to spell out the neither!

Interface variance
The other new feature, as discussed in post number six, is the extension of variance to interfaces. There's not much to add here - it's just the same thing, but using interfaces. Eric gives a really nice example of the practical benefit here, and I'm going to repeat it almost verbatim.

Take a look at this code block. This is another example of something which is illegal in C#3, and legal in C#4:

void FeedAnimals(IEnumerable<Animal> animals) {
  foreach(Animal animal in animals)
    if (animal.Hungry)
      Feed(animal);
}
//...
IEnumerable<Giraffe> adultGiraffes = 
        from g in giraffes where g.Age > 5 select g;
FeedAnimals(adultGiraffes);

Just as earlier on, when we call FeedAnimals(IEnumerable<Animal> animals) we are assigning a 'smaller' type to a 'larger' type:

//Direction of assignability -->
Giraffe                   <   Animal

Giraffe MakeGiraffe()     <   Func<Animal>        //covariance
IEnumerable<Giraffe>      <   IEnumerable<Animal> //covariance

Of course, anywhere else that you reference that assigned-to variable (IEnumerable<Animal>), what comes out will be typed as Animal. All pretty uncontroversial.

In and out
But finally, let's look at the in and out keywords, and how they fit in when designing your own interfaces (or using the upgraded C#4 ones.) Recall one more time the heuristic:

"Stuff going 'in' may be contravariant,
stuff going 'out' may be covariant"

In C# 4.0, IEnumerable<T> has become IEnumerable<out T>. The out marks the IEnumerable as supporting covariance on the T. This means that, as in the example above, you can assign based on something smaller than T.

But it also means that the interface cannot accept the type T as an input! It will only allow the interface to send T out, in whatever fashion you like - but it will never accept a T in. If you try it, the compiler won't allow it. Hence, the name: out.

Reading through this code block should make it clear why:

//the compiler won't allow this, but go with it to see why:
interface ICustom<out T>
{
    T GetFirst();     //ok
    void Insert(T t); //compiler complains
}
//..
ICustom<Giraffe> giraffes = //somehow init;
ICustom<Animal> animals = giraffes;
Animal animal = animals.GetFirst();  //ok
animals.Insert(new Tiger());   //problem - 
                               //backing store is Giraffe

Think of it this way - an IEnumerable<Animal> variable can have an IEnumerable<Giraffe> assigned to it and it will churn out Giraffes typed as Animals all day long. Because of how it's declared, users of the IEnumerable<Animal> variable expect to be dealing with Animals.

But a Tiger is also an animal. What would happen if there were a method on the interface that allowed a user to put an Animal in?

The user could put a Tiger in instead, and the backing store - IEnumerable<Giraffe> - wouldn't be able to cope.

The same in reverse
Now here's a similarly invalid code block, this time using the in keyword:

//the compiler won't allow this, but go with it to see why:
interface ICustom<in T>
{
    T GetFirst();     //compiler complains
    void Insert(T t); //ok
}
//..
ICustom<Animal> animals = //somehow init;
ICustom<Giraffe> giraffes = animals;
giraffes.Insert(new Giraffe()); //ok

Giraffe giraffe = giraffes.GetFirst();  //problem
                               //backing store is Animal

So when a type is marked as out, it's out only. And when a type is marked as in, it's in only too! A type can't be both in and out.

How to read it
So when you read an out type in an interface, read it this way:

interface ICustom<out T>
{
    //You can initialise me using <=T
    //And I will use it as a backing store
    //But I will only send T's out-wards
    //Because T's coming in could be too wide
    //                    for my <=T backing store
}

And for an in type in an interface:

interface ICustom<in T>
{
    //You can initialise me using >=T
    //And I will use it as a backing store
    //But I will only accept T's in-wards
    //Because my >=T backing store is too wide
    //                   to produce T's to send out
}

If it helps, try reading those again - but this time, with the out T interfaces read T as Animal and <=T as Giraffe.

And with the in T interfaces read T as Giraffe and >=T as Animal.

Or, more concisely
Here's out again more concisely:

interface ICustom<out T>
{
    //covariant
    //assign a smaller T, i'll only send it out
}

And for in:

interface ICustom<in T>
{
    //contravariant
    //assign a larger T, i'll only take it in
}

I hope that helps!

The payoff
As Eric points out, the only way to make the above example of FeedAnimals work in C#3 is to use a "silly and expensive casting operation":

FeedAnimals(adultGiraffes.Cast<Animal>());
//or
FeedAnimals(from g in adultGiraffes select (Animal)g);

He goes on:

"This explicit typing should not be necessary. Unlike arrays (which are read-write) it is perfectly typesafe to treat a read-only list of giraffes as a list of animals"

And the example which Eric suggests hypothetically in that post, Matt Hidinger later demonstrates for us using C#4!

The full series
That's about as much as I want to write on the subject!

Below are links to the full series. Bear in mind that these explanatory posts were written prior to the release of C# 4.0. But they are still an excellent programmer's introduction, with much more info than I have covered in this post:

Friday, July 22, 2011

Lambda Expressions in C#

In a previous article I introduced delegates and anonymous delegates. In this article I intend to explain the fundamentals of Lambda expressions by building on our knowledge of delegates. It's useful to get a proper handle on delegates before delving into Lambdas, because Lambdas are just anonymous delegates in different clothing.

A Quick Example
At the end of my post on delegates, I introduced a common scenario where you might want to use an anonymous delegate to find an item from a list:

class AdHocSearch
{
    static void Main()
    {
        List<Book> bookDb = new List<Book>();
        bookDb.Add(new Book(){ title = "Romancing the Stone" });
        bookDb.Add(new Book(){ title = "Wuthering Heights" });
        
        Book foundBook = bookDb.Find(delegate(Book candidate)
        {
            return candidate.title.Equals(
                            "Wuthering Heights");
        });

        Console.WriteLine(foundBook.title);
    }
}

However the same code can easily be refactored to use a Lambda expression (if you use ReSharper just hit Alt+Enter and it will refactor for you):

class AdHocSearch
{
    static void Main()
    {
        List<Book> bookDb = new List<Book>();
        bookDb.Add(new Book(){ title = "Romancing the Stone" });
        bookDb.Add(new Book(){ title = "Wuthering Heights" });
        
        Book foundBook = bookDb.Find(
            candidate => candidate.title.Equals(
                                "Wuthering Heights"));

        Console.WriteLine(foundBook.title);
    }
}

The portion highlighted is the Lambda expression, and it does the same job as the delegate in the first code-block. However it does it in a way which is slightly different - it is more concise, and it's declaration is less explicit.

Let's start from the top - the Lambda operator.

The Lambda Operator
You can tell you are looking at a lambda expression when you see the lambda operator (=>). This operator can be read as "goes to":

x => x * x

So this expression (above) is read as "x goes to x times x".

How do we Get from Delegate to Lambda?
Let's explain the above expression by showing how it would look in familiar, delegate form. Then we can alter it step by step until it becomes a lamba expression again. Here's the fully written-out delegate equivalent of the above statement (below). I have written this in a way which should be familiar from my previous post on delegates:

public delegate int Square(int i);

void Main()
{
    Square square = delegate(int i)
    {
        return i*i;
    };
}

In the code-block above, the first thing to notice is that we have a named delegate type - it's name is Square. This is the first line in the code-block, the declaration that essentially says (in English) "There is a delegate type named Square, it has a single int input parameter, and an int return type."

Inside the body of the Main method, we have then instantiated an instance of Square, (named lower-case square), and assigned an anonymous delegate to it.

Step 1: Anonymise the Delegate Type
Well now, we are going to anonymise the delegate type aswell as the delegate. Anonymising means declaring the same behaviour, but without specifying a name. So in English: "There is a delegate type named Square, it has a single int input parameter, and an int return type".

Since we are no longer giving it a name, we must declare it at the same time as assigning a value to it, and therefore now the whole thing is enclosed within one statement. Remember, we are still naming the variable (lower-case square), but we are no longer naming the type (was init-capped Square):

public delegate int Square(int i);

void Main()
{
    Square square = delegate(int i)
    {
        return i*i;
    };
}

//becomes

void Main()
{
    Func<int, int> square = delegate(int i)
    {
        return i*i;
    };
}

To do this we used a type named Func<T, TResult> (see the definition here), which was introduced in C# 3.5. This class allows us to encapsulate a method as a reference-type, and pass in a couple of generic type parameters describing the argument and return type respectively: <int, int>.

You'll notice that there are several variations on Func, each with a different number of generic type parameters. You can encapsulate a method with up to 16 arguments, and there is a similar family of classes to deal with methods with no return type. Any method you assign to an instance of one of these types has to have matching input parameters and return type, and you can see in the code-block above this is the case.

Step 2: Switch to Lambda Syntax
Now let's convert the delegate on the right side of that assignment operation into a lamda expression:

Func<int, int> square = delegate(int i)
{
    return i*i;
};

//becomes

Func<int, int> square = i => i * i;

And there we have the same statement we started with. It uses the same variables to do the same thing and return the same value - but with different syntax.

The first thing you notice is how concise it is, even though it does exactly the same job. On the left side of the operator, we always have the input parameters.

  • left-of-the-operator - input parameters,
  • right-of-the-operator - expression or statement block

Remember, the operator is read as "goes to". So a lambda is always read as "input (goes to) expression", or "input (goes to) statement block".

Inferred Typing
With the delegate version we had to specify the type of the input parameter, when we said: delegate(int i). However with lambdas the compiler infers the type of i from it's usage. The compiler sees that we are assigning the expression to a strongly-typed Func object with generic type parameteres <int, int>. So it knows and doesn't need to be retold that the input parameter i in the expression is an int.

Don't make the mistake of thinking this is loosely-typed. Loose-typing is not possible in the .NET Framework. This is inferred typing, which is still very much strongly-typed. Type usage is inferred by the compiler on initialisation and cannot change. It is part of the suite of implicit-typing functionality introduced in C# 3.0, including:

In fact, one of the main benefits of lambda's, is they are a simple way to make many previously un-type-safe operations type-safe. Take a quick look at Mauricio Scheffer's blog here, where he has used lambda's to replace string literals. You see this type of usage a lot in .NET frameworks and packages nowadays.

In the first example in the first code-block of this post, without Lambdas or delegates the Find method would only be possible by passing in string literals representing the desired candidate properties. And you can also see, for example, from my post on StructureMap IoC, that statement lambdas are now the preferred initialisation approach (though the previous approach was also typesafe).

Specifying Input Parameters
If you want to override the compiler's inferred usage, you can use brackets on the left-side of the lambda operator to specify input parameters:

Func<int, int> square = (int i) => i * i;
Or, if you have no parameters to pass in, just use empty brackets:
Func<int> noParams = () => 5 * 5;

I have to admit i'm not sure yet where or when this would be useful.

Expression Lambdas vs. Statement Lambdas
So far we've only been using Expression Lambdas, which means that on the right-side of the lambda operator there is one, single expression. This is generally useful when you are trying to apply a function, such as Square above, or trying to apply a predicate, such as the List.Find example at the top of this article.

However you can construct multi-line, procedural lambdas if you wish, by using curly braces:

Action speakToWorld = n =>
{
    string s = n + " " + "World";
    Console.WriteLine(s);
};
speakToWorld("Hello");

Action is ofcourse the cousin of Func, but never returns a value (see the reference page here).

Expression Trees
One final thing to note here is the broader context within which both lambda's and delegates exist. We have moved into an arena where functionality now has the same status as value. What I mean is that we are used to assigning and manipulating values (and objects encapsulating values) within variables. Now it is just as easy to build and manipulate functionality. Expression trees are an example of that.

Earlier we used Func and Action to store our method pointers. These allow truly anonymous references to executable code. But these classes can be assembled and re-assembled in their own right, as components of an Expression object.

Have a quick look at the MSDN on Expression Trees:

"Expression trees represent code in a tree-like data structure, where each node is an expression, for example, a method call or a binary operation such as x < y."
"You can compile and run code represented by expression trees. This enables dynamic modification of executable code, the execution of LINQ queries in various databases, and the creation of dynamic queries."

A quick example from the-code-project:

ParameterExpression parameter1 = 
    Expression.Parameter(typeof(int), "x");

BinaryExpression multiply = 
    Expression.Multiply(parameter1, parameter1);

Expression<Func<int, int>> square = 
    Expression.Lambda<Func<int, int>>(multiply, parameter1);

Func<int, int> lambda = square.Compile();
Console.WriteLine(lambda(5));

In the code-block above, we have done the same thing our simple lambda expression from earlier does - square a number. It is much more verbose, yes, but look at the types on the last two lines before Console.WriteLine. We created a dynamic Expression object, which can be altered and assembled ad-hoc however you like at runtime - and then Compile()'d into executable code, and run - again, all at runtime.

In other words, you can have your code assemble more code as it runs. Or in other words again, you can have your code treat itself in the same, flexible way it used to only be able to treat data and values.

Aside from generating dynamic queries, what does this allow us to do? As this stack-overflow page points out, we can hand the power of code customisation over to users. Ever made a 'form builder', allowing users to customise post-driven dynamic webpages? I can't imagine how we did that in the past but they've just got a whole lot easier, and semantically correct. Or what about user-customised workflows? Ditto.

Further Reading

Thursday, July 21, 2011

Introduction to Delegates in C#

What is a delegate? Most articles dive straight into the technical stuff, but I think it makes it easier to learn when you start by looking at the metaphor - I will dive right into the practical example straight afterwards.

So first of all, what makes the word 'delegate' such an apt keyword?

The Metaphor
According to Wikipedia, a delegate is a person who is similar to a representative, but "delegates differ from representatives because they receive and carry out instructions from the group that sends them, and, unlike representatives, are not expected to act independently."

Imagine a government department is employing two private companies to carry out litter collection across two districts. The government are responsible for preparing the contracts, and intend to use a standard template contract for both private companies. But the two private companies have different operating practices, and each want to tailor their respective contracts to suit their own particular needs.

They each send a delegate to meet the government and make amendments to their respective contracts. Each delegate is briefed by their respective companies before they leave, so that they know exactly what their instructions are. Each delegate arrives intending to do the same thing - amend a contract. But their instructions and boundaries on how they can do that are different - and are defined clearly by the respective senders.

I'll end the metaphor here, because I don't want to lock into one example. But having that sense of what a delegate is should make the rest of this article easier to read.

Declaring a Delegate Type
First, let's declare a publicly accessible delegate type:

//Declare a delegate type
public delegate void BookProcessingDelegate(Book b);

It is important at this stage to understand that what we have defined is not a delegate but a delegate type. Each instance we later instantiate of this type will be an actual delegate.

Delegates are usually declared not as class members, but as peers of classes or structs, i.e. at the same level. This is because declared delegate types are types in their own right. They can ofcourse, also be declared nested inside a class (just like a class can be declared nested inside a class).

In other words, a delegate is an example of a reference-type, and is stored on the heap and accessed via a pointer (see my article on types, the stack and the heap).

Instantiating and Assigning a Delegate Method
Having declared our delegate type, there are three things we need to do to make use of it:

  1. Define a method (write some instructions)
  2. Instantiate a delegate and assign the method to it (brief the delegate of our instructions)
  3. Send the delegate off to do business on our behalf

I'm going to use a variation of the 'classic' delegate example, and note that I've commented numerically the implementation of the bullet points above:

//Declare a delegate type
public delegate void BookProcessingDelegate(Book b);

public class Book
{
    public string title { get; set; }
}

public class BookDB
{
    private IList<Book> books;

    public void ProcessBooks(BookProcessingDelegate del)
    {
        foreach(Book b in books)
        {
            // Call the delegate method
            del(b);
        }
    }
}

class Program
{
    //1. Define a method
    private static void PrintBookTitle(Book b)
    {
        Console.WriteLine(b.title);
    }

    static void Main()
    {
        BookDB bookDb = new BookDB();

        //2. Instantiate a delegate, and assign the method
        BookProcessingDelegate printTitles = PrintBookTitle;

        //3. Send the delegate off to do work on our behalf
        bookDb.ProcessBooks(printTitles);    
    }
}

When we assign the method to the delegate (item 2 above), we are actually assigning to the delegate a pointer to the method. Then, when the BookDB.ProcessBooks method uses (invokes) the delegate, it follows the pointer and actually invokes the referenced method.

Logically, it is exactly the same as if the body of the referenced method had been declared inside of BookDB. But ofcourse it wasn't, and that's the key to the usefulness of delegates - i'll discuss this in more detail shortly.

But first of all let's explore two more ideas - anonymous delegates, and multicast delegates.

Anonymous Delegates
In the above example, we assigned a named method which was a private class member (namely PrintBookTitle). If that method is only going to be used for this one specific purpose, it can be much more convenient to declare an anonymous method.

What follows is exactly the same code block as above, but now the PrintBookTitle method has been anonymised, causing steps 1 and 2 to become one statement:

class Program
{
    static void Main()
    {
        BookDB bookDb = new BookDB();

        //1. Define a method AND
        //2. Instantiate a delegate, and assign a method
        BookProcessingDelegate printTitles = delegate(Book b) {
            Console.WriteLine(b.title);
        };

        //3. Send the delegate off to do work on our behalf
        bookDb.ProcessBooks(printTitles);    
    }
}

Anonymous delegates are more concise, and they are the foundation of lambda expressions, which I discuss in another article.

Multicast Delegates
It's another odd word, but multicast is based on another metaphor. Check out the link to get a quick sense of that, but it's simple enough anyway. You can assign more than one method to a delegate. The delegate will respond when invoked, by executing each method in order:

class Program
{
    private static void PrintLower(Book b) {
        Console.WriteLine(b.title.ToLower());
    }

    private static void PrintUpper(Book b) {
        Console.WriteLine(b.title.ToUpper());
    }

    static void Main()
    {
        //Instantiate a delegate, and assign TWO methods
        BookProcessingDelegate printTitles = PrintUpper;
        printTitles += PrintLower;

        //You can also use the -= operator to remove methods
        //from the 'invocation list'
        printTitles -= PrintLower;
    }
}

You can add and remove methods to your heart's content. The resulting list of methods which must be called on invocation of the delegate is referred to as the delegate's invocation list.

If the delegate has a return value, only the value returned by the last method in the invocation list will be returned. Similarly, if the delegate has an out or ref parameter, the parameter will end up being equal to the value assigned in the last method in the invocation list (see also my article on parameter passing in C#).

What Are Delegates Useful For?
As we have seen, delegates give us the ability to assign functionality at runtime. This adds to our toolbox of assignment operations - we all know how easy it is to assign values, or references to objects. Now we can assign functionality too.

Look back to the first, main code-block of this article. It should be clear that any class, anywhere in your application that chooses to call BookDB.ProcessBooks can do so and provide it's own specific, tailored functionality. The Program class happens to have sent one particular type of functionality (printing to the console), but any delegate can point to any method implementation.

Without delegates, the creators of the BookDB class would have to have thought up in advance every possible thing that callers might want to do with the Book list - and provide lots of methods to cover all those bases. In doing so, the BookDB class would be taking on responsibilities that are not within it's problem domain.

The other way to achieve the same functionality without delegates would be for the BookDB to expose it's internal list as a property, or via an access method. The Program class could then iterate over the collection itself. But this approach would mean that the Program class was shouldering responsibilities that are not within it's domain - iterating over a collection.

Delegates therefore provide an elegant solution which helps achieve the Seperation of Concerns principle within your application architecture.

A Common Usage - Ad-Hoc Searching
There are lots of places in the .NET Framework where you can use delegates, and their newer equivalent, lambda expressions. We'll take a look at one example, by altering our BookDB code a little. Let's say you want to find a book with a particular title, from a list of Book items:

class AdHocSearch
{
    static void Main()
    {
        List<Book> bookDb = new List<Book>();
        bookDb.Add(new Book() { title="Romancing the Stone" });
        bookDb.Add(new Book() { title="Wuthering Heights" });

        Book foundBook = bookDb.Find(delegate(Book candidate)
        {
            return candidate.title.Equals("Wuthering Heights");
        });

        Console.WriteLine(foundBook.title);
    }
}

If you couldn't use a delegate, you would have to manually iterate over the list. But fortunately for us, the List<T>.Find() method will accept a delegate function which describes how to apply a condition which determines a match for us. This leaves the job of iterating the collection, with it's associated efficiency concerns, in the correct problem domain - the domain of the List.

Quick Runthrough
For a nice, quick, concise primer on the subjects I've discussed above, check out this Youtube video:

I don't think it stands up as a learning resource in it's own right, but as a primer it's quite well organised.

Further Reading
This is quite a nice delegates tutorial on MSDN.