Skip to content
Theory · Lambdas and LINQ · lesson 1 of 10

Methods as values

What you'll learn

What you'll learn:

  • store a method in a variable and pass it to another method through Func, Action and Predicate;
  • write lambda expressions and explain what a closure captures;
  • filter, transform and sort items with LINQ methods and compute totals over them;
  • explain when a LINQ query runs and what ToList() is for.
A card Twice sits in the machine Transform, tiles of 1, 2 and 3 dots come out with 2, 4 and 6, and a card Square waits beside it

Picture a recipe card. You can cook from it yourself, or you can hand the card to a friend, who cooks the dish when it is needed. The card can go into a drawer, be swapped for another one or be passed on, and the recipe never has to be rewritten.

So far a method has only been called, like the copier from the theme on methods: you wrote Max(3, 8), and it ran at once. You cannot hand a copier to a friend, but you can hand over a recipe card. In C# a method can be handed over like that card: put into a variable or given to another method, and called later.

Why is this useful? Say one place in a program has to double every number of a list, and another has to square each one. The two methods come out the same except for one line with the calculation. It is handier to write one method that walks the list and to hand it the calculation, like a recipe card.

Let's start with a variable that holds a method:

C#
Func<int, int> f = Twice;
Console.WriteLine(f(5)); // 10
f = Square;
Console.WriteLine(f(5)); // 25

int Twice(int x) => x * 2;
int Square(int x) => x * x;

Let's go through the program line by line:

  1. Func<int, int> f = Twice; puts the method Twice itself into the variable f. There are no parentheses after Twice: the method is not called here, it is handed over.
  2. f(5) calls the method that f holds right now. That is Twice, and it returns 10.
  3. f = Square; puts another method into the same variable.
  4. The second call f(5) now runs Square and prints 25.

A variable that holds a method

The type Func<int, int> describes a method that takes an int and returns an int. A type whose variable holds a reference to a method is called a delegate. You call a delegate variable the same way as the method itself: the name and the arguments in parentheses.

A method name without parentheses, like Twice in the first line, is called a method group. The word "group" is there because one name may cover several overloads, and the compiler picks the one that fits the delegate.

Here the recipe card and the delegate part ways. Any recipe can be written on a card, but a Func<int, int> variable takes only a method that takes an int and returns an int. The method Greet(string name) does not fit there: the compiler checks the parameters and the return type.

Ready-made delegates

.NET has ready-made delegates of three kinds. They cover almost every task, so your own delegate types, declared with the word delegate, are rarely written. Interviewers like to ask this: how do Func, Action and Predicate differ?

Func returns a result

The angle brackets list the parameter types, and the last one is the return type:

  • Func<int>: no parameters, returns an int;
  • Func<int, int>: takes an int, returns an int;
  • Func<int, int, int>: takes two ints, returns an int.

Action returns nothing

Action describes a void method. The angle brackets hold only parameter types: Action<string> takes a string and returns nothing. Action without angle brackets takes nothing.

Predicate checks a value

Predicate<T> takes one value of type T and returns a bool: whether the value fits or not. Such a check is called a predicate.

C#
Action<string> greet = Greet;
greet("Anna"); // Hello, Anna!
Predicate<int> isEven = IsEven;
Console.WriteLine(isEven(4)); // True
Console.WriteLine(isEven(7)); // False

void Greet(string name) => Console.WriteLine($"Hello, {name}!");
bool IsEven(int n) => n % 2 == 0;

greet("Anna") runs Greet and prints a greeting. isEven(4) calls IsEven, which answers true.

A method as an argument

Back to the task from the start of the lesson. A method parameter can be a delegate too, and then the method gets the calculation from outside:

C#
List<int> nums = [1, 2, 3];
Console.WriteLine(string.Join(" ", Transform(nums, Twice)));  // 2 4 6
Console.WriteLine(string.Join(" ", Transform(nums, Square))); // 1 4 9

List<int> Transform(List<int> items, Func<int, int> f)
{
    List<int> result = [];
    foreach (int n in items)
    {
        result.Add(f(n));
    }
    return result;
}

Let's see what Transform does:

  1. The parameter f of type Func<int, int> accepts any method from int to int.
  2. The loop walks the list and calls f(n) for each item.
  3. The results go into a new list result, which the method returns.
  4. The call Transform(nums, Twice) hands over Twice as a card, and Transform(nums, Square) hands over another card. The loop is written once.

Ready-made .NET methods take delegates too. A list has the method FindAll: you pass it a Predicate<T>, and it returns a new list of the items that fit. An example call: nums.FindAll(IsEven).

Common mistake

The method was passed with parentheses. Here Apply gets a method and a number and calls that method with the number x:

C#
Console.WriteLine(Apply(Twice(), 5)); // ✗

int Apply(Func<int, int> f, int x) => f(x);
int Twice(int x) => x * 2;
TEXT
error CS7036: There is no argument given that corresponds to the required parameter 'x' of 'Twice(int)'

The program does not compile. Parentheses after a name mean a call, so the compiler tries to call Twice with no arguments, and Twice needs x. To pass the method itself, write its name without parentheses: Apply(Twice, 5).

Key points

  • A delegate is a type whose variable holds a reference to a method; you call it like the method itself: f(5).
  • Func returns a result, and its type comes last in the angle brackets; Action returns nothing; Predicate<T> answers with a bool.
  • To pass a method, write its name without parentheses: Transform(nums, Twice). With parentheses it is already a call.