7 min read

Delegates

Crash Course

Introduction

Let’s assume you have the following two types defined to represent ships in a space RTS game:

enum ShipType { Frigate, Destroyer, Cruiser, Battleship }

class Ship
{
    public ShipType Type { get; }
    public int TeamNumber { get; }

    public Point Location { get; set; }
    public int RemainingHealth { get; set; }
}

Throughout your game’s code, you may want to look through all of the ships and find ones that meet certain conditions. For example, you might have this code, which looks for any ship whose health has reached 0, possibly to remove it from the game:

public List<Ship> FindDeadShips(List<Ship> allShips)
{
    List<Ship> foundShips = new List<Ship>();

    foreach (Ship ship in allShips)
        if (ship.RemainingHealth <= 0)
            foundShips.Add(ship);

    return foundShips;
}

Or this, which might be useful to find any friendly battleships, possibly to alert it to come help:

public List<Ship> FindFriendlyBattleships(List<Ship> allShips)
{
    List<Ship> foundShips = new List<Ship>();

    foreach (Ship ship in allShips)
        if (ship.TeamNumber == 1 && ship.Type == ShipType.Battleship)
            foundShips.Add(ship);

    return foundShips;
}

The code, in these cases, is almost identical, except the condition right in the middle: the part that makes a decision about whether to include the ship in the result list or not.

Now, there’s a way we could manage this with inheritance and polymorphism, but in this case, it would be nice to have a simpler solution. It would be nice if we could pass in the logic to use there as a parameter.

We can do that with delegates. A delegate is a type that represents a method. By using delegate types, we can pass around methods in the same way we pass around integers and strings.

Defining a Delegate Type

To start, we must define a delegate type:

delegate bool ShipEvaluator(Ship ship);

This defines a new type, the same way we define new enumerations, classes, structs, and interfaces, and you can place a delegate definition anywhere you can place one of those. If you’re using top-level statements, all type definitions must go at the bottom, after everything else. If you’re using the whole class Program and public static void Main thing, it should just go somewhere outside of the class Program. Of, of course, you can make a new .cs file to place it in.

Defining a delegate type starts with the delegate keyword.

After that comes the return type, which I chose as bool here. In C#, types matter. While delegates will allow us to pass code around like data, not all methods are interchangeable with everything else. The return type as well as the count and types of the parameters will determine what can be swapped for what.

In this particular case, we’ve defined a new delegate type with the name ShipEvaluator, whose return type is bool and that has a single parameter of type Ship.

A variable whose type is ShipEvaluator will be able to contain any method whose return type is also bool and that has a single parameter of type Ship, regardless of what the logic within the method does.

Here is a simple illustration. Let’s suppose you have several methods defined like this:

bool IsDead(Ship ship) => ship.RemainingHealth <= 0;
bool IsTeam1Battleship(Ship ship) => ship.Team == 1 && ship.Type == ShipType.Battleship;

note

I’ve intentionally used expression bodies for these two methods, but we could have used block bodies with the curly braces and return keywords, had we wanted. The style, in this case, is incidental.

Now we can make a single Find method to replace our earlier FindDeadShips and FindFriendlyBattleships methods:

public List<Ship> Find(List<Ship> allShips, ShipEvaluator shipEvaluator)
{
    List<Ship> foundShips = new List<Ship>();

    foreach (Ship ship in allShips)
        if (shipEvaluator(ship)))
            foundShips.Add(ship);

    return foundShips;
}

The main difference is that this method includes an extra parameters: ShipEvaluator shipEvaluator. That’s the delegate type we made, and what allows us to pass in different methods at different times, to change how Find works.

We use whatever method was supplied to us in the if statement by calling shipEvaluator(ship). This code will actually call the passed in method, running it, regardless of what the method was, and then working with its result.

A second way to call the delegate is by calling Invoke on it:

if (shipEvaluator.Invoke(ship))
    foundShips.Add(ship);

Some people prefer this style, others prefer the original style.

We can then delete FindDeadShips and FindFriendlyBattleships and replace them with method calls like this:

List<Ship> allDeadShips = Find(allShips, IsDead);

And:

List<Ship> friendlyBattleships = Find(allShips, IsTeam1Battleship);

Note that when you assign a value (a method) to a delegate-typed variable, you just supply the method name, without parentheses or arguments.

note

Putting a method into a delegate variable does not automatically call that method. It will be called later on, as we saw earlier with shipEvaluator(ship).

Even better, as we come up with a need for other filter mechanisms (for example, ships near this other ship, ships that have full health, etc.) we don’t need to copy and paste all of that code. We can just define a new method that captures the unique check, and call Find instead.

note

You are, obviously, not limited to just returning bool or even a single parameter. Adapt your delegate types to fit the needs of your situation. You can also make void delegate types.

Action and Func

While you can define your own delegate types, you actually rarely need to. If you’re willing to use what already exists.

There is a whole family of delegate types already defined that cover nearly all scenarios. These are the Action and Func delegate types. The Action delegate types all have a void return type, while the Func types return a value. In both cases, they make use of generics to provide you with optimal flexibility.

For example, Action represents any method with a void return type and no parameters.

Action<T> represents any method with a void return type and a single parameter with a generic type. If you wanted to represent a method that looked like void DoStuff(int number), then you’d use an Action<int>.

Action<T1, T2> represents a method with a void return type and two generic parameters. A method like void DoStuff(string text, double number) could be represented with the delegate type Action<string, double>.

There’s quite a few Action delegates that contain all the way up to 16 parameters. If you need more than that, something has gone terribly wrong.

The Func delegates work for things that return a value, and their return type is generic as well. The method int GetNumber() could be stored in the delegate type Func<int>. The method string Combine(int number, bool thing) could be stored in the delegate type Func<int, bool, string>.

Thus, while you can define your own delegate type, there is hardly a use where one of the Action or Func delegate types doesn’t have you covered. (Though sometimes, your own more specific name helps clarify what’s going on better than a general-purpose “action” or “func” does.)

Lambda Expressions

It is hard to talk about delegates and not bring up a useful tool in C# called a lambda expression. We won’t be covering every detail about lambda expressions–just the basics–but that’s worth doing.

A lambda expression is a way to define a single-use, unnamed method directly in place. This makes it so you don’t need to go through the formality of coming up with a name for the method and can be more readable.

This is another one of those things where it is just easier to show than describe, so our earlier Find method could have been called like this instead of having a specific IsDead method:

List<Ship> allDeadShips = Find(allShips, ship => ship.RemainingHealth <= 0);

That ship => ship.RemainingHealth <= 0 is a lambda expression. It defines a method–without giving it a name–that does exactly what our earlier, named method (IsDead) does. You can see that this uses an expression body, and the parameter type and return type are both inferred from the method’s usage itself.

note

This particular one might come out as being extra strange, simply because it looks like you’ve got two arrows pointing inward: => ... <=. That’s incidental. The => is the lambda operator (we also saw it with switches) while the <= is a “less than or equal” operator. If we did the same thing with the IsTeam1Battleship method, we wouldn’t see the two arrow thing come up. We’d just have ship => ship.TeamNumber == 1 && ship.Type == ShipType.Battleship.

Lambda expressions require a little adjusting to, but once you’re comfortable with them, they are an extremely powerful tool to add to your arsenal.

The Where Method

There’s a lot of ground we can’t cover in this crash course, but I think it is wise to point out that our Find method is something that already exists for lists (kind of). It is defined as an extension method, and is called Where:

List<int> numbers = new List<int>();
// Do something here to populate the numbers.
// ???

foreach (int number in numbers.Where(number => number > 0)) // Lambda expression for positive numbers only
    Console.WriteLine(number + " is positive.");

note

The Where method is in the System.Linq namespace, and will require adding a new using directive: using System.Linq;.