Skip to content
wannacode

Lambdas and delegates

In short: A lambda is a short function without a name that you pass as a value; in C# its type is a delegate such as Func, Action or Predicate.

Problem

A whole class for one comparison

The orders need to be sorted by total. You know how from the "Collections" module: Sort with an object of a class that implements IComparer<Order>.

Order
class Order
{
public string Customer { get; }
public double Total { get; }
public Order(string customer, double total)
{
Customer = customer;
Total = total;
}
}
class ByTotal : IComparer<Order>
{
public int Compare(Order a, Order b)
{
return a.Total.CompareTo(b.Total);
}
}
var orders = new List<Order>
{
new Order("Maria", 75.50),
new Order("Georgi", 12.90),
new Order("Elena", 40.00)
};
orders.Sort(new ByTotal());
foreach (Order o in orders)
{
Console.WriteLine($"{o.Customer}: €{o.Total:F2}");
}
Output
Georgi: €12.90
Elena: €40.00
Maria: €75.50

It works. But for one comparison you write a whole class: class ByTotal : IComparer<Order>, public int Compare, braces, and then new ByTotal(). Only one line says something: a.Total.CompareTo(b.Total). And the class sits far from the place where you use it. You want to pass just the rule.

The idea

A lambda: just the rule

The same sort, on one line:

orders.Sort((a, b) => a.Total.CompareTo(b.Total));

(a, b) => a.Total.CompareTo(b.Total) is called a lambda expression, or a lambda for short. To the left of => are the parameters, to the right is the body. It is the Compare method without a name, without types and without return.

A lambda can be written in three ways:

  • One parameter can go without parentheses: o => o.Total > 50. With no parameters it is () => ....
  • The types of the parameters are usually left out, because the compiler knows them from Sort. You can also write them: (Order a, Order b) => ....
  • A body with several lines goes in { }, and then return is required.
Comparison<Order> byTotal = (a, b) => a.Total.CompareTo(b.Total);
Comparison<Order> byTotalLong = (Order a, Order b) =>
{
double diff = a.Total - b.Total;
return diff < 0 ? -1 : diff > 0 ? 1 : 0;
};
orders.Sort(byTotalLong);
foreach (Order o in orders)
{
Console.WriteLine($"{o.Customer}: €{o.Total:F2}");
}
Output
Georgi: €12.90
Elena: €40.00
Maria: €75.50
A lambda is a value: you can put it in a variable and pass it later. The type of the variable here is Comparison<Order>. What that is comes in the next card.

The idea

A delegate: the type of the rule

How does C# know that (a, b) => ... is a comparison of two orders? From the type: Sort has a version that takes a Comparison<Order>. That is a delegate: a type that describes a method, its parameters and its result. Comparison<Order> is "a method that takes two orders and returns an int". The lambda becomes the body of such a method.

You can make your own delegate. You write it like a method, with the word delegate and no body:

delegate double PriceRule(double price);
PriceRule student = price => price * 0.8;
PriceRule minusFive = price => price - 5;
Console.WriteLine(student(50));
Console.WriteLine(minusFive(50));
Output
40
45
student is an object of type PriceRule. You call it like a method, with parentheses: student(50). Two price rules, and not a single class. C# prints 40, not 40.0: the number is whole.

PriceRule student = price => price * 0.8;
PriceRule minusFive = price => price - 5;
Console.WriteLine(student(50));
Console.WriteLine(minusFive(50));
Step by step, in words
  1. The lambda is an object of type PriceRule, and student points to it. Its body does not run yet.
  2. A second rule, a second object. Not a single class is written.
  3. student(50) passes 50 to the lambda: price is 50, and price * 0.8 gives 40.
  4. minusFive(50): the same number, a different rule, 45.
More precisely, if you are curious
  • In Java a lambda goes where an interface with one method (a functional interface) is expected. In C# a lambda cannot go to an interface, even if it has one single method:
interface IPriceRule
{
double Apply(double price);
}
IPriceRule student = price => price * 0.8;
Compile error
error CS1660: Cannot convert lambda expression to type 'IPriceRule' because it is not a delegate type

That is why in C# a rule passed as a lambda is a delegate, not an interface.

  • student(50) is short for student.Invoke(50). And GetType() shows that the object really is of the delegate type:
PriceRule student = price => price * 0.8;
Console.WriteLine(student.GetType().Name);
Console.WriteLine(student.Invoke(50));
Output
PriceRule
40

The idea

The built-in ones: Predicate, Func, Action

You do not need to write a delegate for every rule. System has built-in ones for the most common cases:

  • Predicate<T>: takes a T, returns a bool. It is a condition: "is the order big?".
  • Func<T, TResult>: takes a T, returns a TResult. The last type in the < > is always the result. It turns one thing into another: an order into a label.
  • Action<T>: takes a T, returns nothing. It does something with it: prints it, saves it.
  • Func<TResult>: takes nothing, returns a TResult. It gives you a new object when you ask.

All of them are called the same way, with parentheses, like a method.

Predicate<Order> big = o => o.Total >= 50;
Func<Order, string> label = o => o.Customer.ToUpper();
Action<string> print = text => Console.WriteLine("-> " + text);
Func<Order> guest = () => new Order("Guest", 0);
var order = new Order("Maria", 75.50);
Console.WriteLine(big(order));
print(label(order));
Console.WriteLine(guest().Customer);
Output
True
-> MARIA
Guest

In Java, rules are objects with methods that combine them: conditions are joined with and, or and negate ("not"), and functions are chained one after another with andThen. Delegates in C# have no such methods. The new rule is a new lambda that calls the old ones: o => big(o) && maria(o). Two functions one after another are also combined with a lambda, which a method returns:

static Func<double, double> Then(Func<double, double> f, Func<double, double> g)
{
return x => g(f(x));
}
Func<double, double> withVat = price => price * 1.2;
Func<double, double> rounded = price => Math.Round(price, 2);
Func<double, double> finalPrice = Then(withVat, rounded);
Console.WriteLine(finalPrice(10.555));
Output
12.67

Then takes two delegates and returns a third one: it calls the first, and passes its result to the second.

More precisely, if you are curious
  • There are versions with more parameters: Func<T1, T2, TResult> takes two and returns a result, Action<T1, T2> takes two and returns nothing. They go up to 16 parameters.
  • Predicate<Order> and Func<Order, bool> describe the same thing, but they are different types. A variable of one cannot be passed where the other is expected.
  • Delegates for actions can be added up with +=. Then one call calls all of them in order:
Action<string> notify = text => Console.WriteLine("Email: " + text);
notify += text => Console.WriteLine("SMS: " + text);
notify("your order has shipped");
Output
Email: your order has shipped
SMS: your order has shipped

Try it

Combined conditions

What will the program print?

Predicate<Order> big = o => o.Total >= 50;
Predicate<Order> maria = o => o.Customer == "Maria";
Predicate<Order> bigAndMaria = o => big(o) && maria(o);
Predicate<Order> bigOrMaria = o => big(o) || maria(o);
Predicate<Order> notBig = o => !big(o);
var small = new Order("Maria", 30);
var large = new Order("Georgi", 80);
Console.WriteLine(bigAndMaria(small));
Console.WriteLine(bigOrMaria(small));
Console.WriteLine(notBig(large));
Answer

See it

Lists take lambdas

List<T> has methods that take a rule instead of a loop:
var cart = new List<string> { "bread", "", "milk", " ", "coffee" };
int removed = cart.RemoveAll(item => string.IsNullOrWhiteSpace(item));
cart.ForEach(item => Console.WriteLine(item));
Console.WriteLine($"Removed: {removed}");
Console.WriteLine(cart.Find(item => item.StartsWith('m')));
Console.WriteLine(cart.Exists(item => item.Length > 5));
List<int> lengths = cart.ConvertAll(item => item.Length);
Console.WriteLine(string.Join(", ", lengths));
Output
bread
milk
coffee
Removed: 2
milk
True
5, 4, 6
  • RemoveAll(condition) removes all elements for which the condition is true, and returns how many it removed. The condition is a Predicate.
  • ForEach(action) does the action with each element. It is an Action.
  • Find(condition) returns the first element for which the condition is true, or null if there is none. Exists says whether there is at least one.
  • ConvertAll(conversion) makes a new list from the results. Here the words become a list of their lengths.

Dictionary has no such methods. Java's computeIfAbsent and forEach are not there, and byCity.ForEach(...) does not compile:

Compile error
error CS1061: 'Dictionary<string, List<string>>' does not contain a definition for 'ForEach' and no accessible extension method 'ForEach' accepting a first argument of type 'Dictionary<string, List<string>>' could be found (are you missing a using directive or an assembly reference?)

You go through a dictionary with foreach, and grouping on one line comes in the next lesson.

The idea

A method by name, without parentheses

Often a lambda only passes its parameter to an existing method: s => Console.WriteLine(s). Then pass the method itself, by name and without parentheses:
var names = new List<string> { "maria", "georgi" };
names.ForEach(Console.WriteLine);
Func<string, int> parse = int.Parse;
var vips = new List<string> { "maria" };
Predicate<string> isVip = vips.Contains;
Func<string, string> upper = s => s.ToUpper();
Func<List<string>> newList = () => new List<string>();
Console.WriteLine(upper("elena"));
Console.WriteLine(parse("42") + 1);
Console.WriteLine(isVip("georgi"));
List<string> list = newList();
list.Add("coffee");
Console.WriteLine(string.Join(", ", list));
Output
maria
georgi
ELENA
43
False
coffee

The name of a method without parentheses is called a method group: the group is all the methods with that name. The compiler picks the one that fits the delegate. Console.WriteLine has many versions, and ForEach wants an Action<string>, so it takes WriteLine(string). Java has a separate notation for this, the method reference with ::. There are four kinds, but in C# only two work by name:

  • a method of a specific object: vips.Contains is name => vips.Contains(name);
  • a static method: int.Parse is s => int.Parse(s), and Console.WriteLine is s => Console.WriteLine(s);
  • a method of the parameter: there is no short form. Java's String::toUpperCase is a lambda in C#, s => s.ToUpper();
  • a constructor: no short form either. ArrayList::new in C# is () => new List<string>().

If the lambda does something more, for example s => s.Trim().ToUpper(), leave it a lambda.

See it

Sorting by several fields

The Comparison<T> delegate has no methods like comparing and thenComparing in Java. The rule for several fields is one lambda: if the comparison by the first field is not 0, it decides, otherwise it compares the next one.

The students are sorted by grade, from the highest, and by name when the grades are equal:

var students = new List<Student>
{
new Student("Ivan", 5.50),
new Student("Ana", 6.00),
new Student("Peter", 5.50),
new Student("Boris", 4.75)
};
students.Sort((a, b) =>
{
int byGrade = b.Grade.CompareTo(a.Grade);
return byGrade != 0 ? byGrade : string.Compare(a.Name, b.Name);
});
Output
Ana 6
Ivan 5.5
Peter 5.5
Boris 4.75

b.Grade.CompareTo(a.Grade) puts the higher grade first. When the grades are equal, byGrade is 0 and string.Compare(a.Name, b.Name) compares the names alphabetically. It reads from top to bottom: "by grade in reverse, and when equal, by name".

More precisely, if you are curious
  • Without a type on the left, the compiler does not know what delegate the lambda is, because it does not know what a and b are:
var byTotal = (a, b) => a.Total.CompareTo(b.Total);
Compile error
error CS8917: The delegate type could not be inferred.

The fix is the delegate type on the left:

Comparison<Order> byTotal = (a, b) => b.Total.CompareTo(a.Total);
orders.Sort(byTotal);
  • With parameter types, var compiles, but it gives Func<Order, Order, int>. It describes the same thing as Comparison<Order>, but it is a different type, and Sort does not accept it:
var byTotal = (Order a, Order b) => a.Total.CompareTo(b.Total);
orders.Sort(byTotal);
Compile error
error CS1503: Argument 1: cannot convert from 'System.Func<Order, Order, int>' to 'System.Collections.Generic.IComparer<Order>?'

The compiler tries the version of Sort with IComparer, which is why that is in the message.

  • Sorting that reads like comparing and thenComparing in Java is in C#'s LINQ: OrderByDescending(s => s.Grade).ThenBy(s => s.Name). It is in the next lesson.

Try it

What does swapping a and b reverse?

Here byGradeThenName compares by grade, then by name, and Sort gets a lambda that calls it with a and b swapped.

What will the program print?

var students = new List<Student>
{
new Student("Ivan", 5.50),
new Student("Ana", 6.00),
new Student("Peter", 5.50),
new Student("Boris", 4.75)
};
Comparison<Student> byGradeThenName = (a, b) =>
{
int byGrade = a.Grade.CompareTo(b.Grade);
return byGrade != 0 ? byGrade : string.Compare(a.Name, b.Name);
};
students.Sort((a, b) => byGradeThenName(b, a));
students.ForEach(s => Console.WriteLine($"{s.Name} {s.Grade}"));
Answer

Try it

Find the bug

The program should print the prices with their numbers: 1. €12.00 and so on. It stops with an error at the very first price. Which line is the bug on?

Pick the line with the bug.

More precisely, if you are curious

Here C# is different from Java. There, a lambda gets a copy of the value and cannot change the local variable at all. In C#, the lambda can change it too: the sum from Java compiles here and prints 127:

var prices = new List<double> { 12, 75, 40 };
double total = 0;
prices.ForEach(p => total += p);
Console.WriteLine(total);
Output
127

And since it sees the variable itself, the lambda also sees a change made after it:

double rate = 0.2;
Func<double, double> discount = price => price * (1 - rate);
Console.WriteLine(discount(100));
rate = 0.5;
Console.WriteLine(discount(100));
Output
80
50

If the rule has to remember a value, pass it as a parameter. For a sum, though, there is a clearer way, Sum, in the next lesson.

Exercise

Rules as values

Solve the tasks in your editor.

1. Sorting by several fields. A class Product with a name, a category and a price. Sort the products by category, within a category from the most expensive, and by name when the price is equal, with one Sort and one lambda. The products are: mouse, accessories, €25; laptop, computers, €1299; keyboard, accessories, €49.90; tablet, computers, €399; headphones, accessories, €49.90.

Output
Accessories | Headphones | €49.90
Accessories | Keyboard | €49.90
Accessories | Mouse | €25.00
Computers | Laptop | €1299.00
Computers | Tablet | €399.00

2. Filtering orders. Add a city to Order. Write a method Select(List<Order> orders, Predicate<Order> rule) that returns a new list with only the orders for which the rule is true. Make two rules, sofia and big (at least €50), and print the orders for which both are true, the ones outside Sofia and the ones "in Sofia or at least €100". Each of the three rules is a lambda that calls sofia and big. The orders: Maria, Sofia, €75.50; Georgi, Varna, €12.90; Elena, Sofia, €40; Dimitar, Plovdiv, €120.

Output
Sofia, €50 or more:
Maria, Sofia: €75.50
Outside Sofia:
Georgi, Varna: €12.90
Dimitar, Plovdiv: €120.00
Free delivery (Sofia or €100 or more):
Maria, Sofia: €75.50
Elena, Sofia: €40.00
Dimitar, Plovdiv: €120.00
3. Discounts one after another. Make three discounts as Func<double, double>: a student discount (−10%), a Black Friday discount (−€20) and "not below zero". Chain them with the Then(f, g) method from the lesson in two orders: the student one first and the Friday one first. Try it with €120 and with €15. Does the order matter?
Output
€120.00 -> student, then Friday: €88.00; Friday, then student: €90.00
€15.00 -> student, then Friday: €0.00; Friday, then student: €0.00

More tasks

  • Cleaning a list. From a list of emails, remove the empty ones and the ones without @ with one RemoveAll, then print the rest with ForEach(Console.WriteLine).
  • Students by class. A list of students and their classes ("10A", "10B"). For each class, print the names with FindAll and a lambda that compares the class, and ConvertAll that takes the name.
  • Your own delegate. Make delegate bool Validator(string value); and three validators as lambdas: non-empty text, digits only (char.IsDigit for each character), length up to 10. Try them on a few strings.

Test

Check yourself

1. Where can a lambda expression go?
2. Which built-in delegate takes an order and returns true or false?
3. What can replace the lambda s => int.Parse(s)?
4. byGradeThenName compares by grade, then by name. What does students.Sort((a, b) => byGradeThenName(b, a)) reverse?
5. In a loop for (int i = 0; i < 3; i++) each lambda prints i. What do the three print when called after the loop?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·