delegate bool ShipEvaluator(Ship ship);
.ShipEvaluator e = IsDead;
.delegateVariable(argument1, argument2)
or delegateVariable.Invoke(argument1, argument2);
.Action
and Func
delegate types are generic, and you can usually use one of those instead of defining your own. For example, Func<Ship, bool>
is equivalent to the ShipEvaluator
delegate type above.Find(ships, ship => ship.RemainingHealth > 0)
.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.
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.)
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.
Where
MethodThere’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;
.