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>.
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}");}Georgi: €12.90Elena: €40.00Maria: €75.50It 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 thenreturnis 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}");}Georgi: €12.90Elena: €40.00Maria: €75.50Comparison<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));4045student 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
- The lambda is an object of type PriceRule, and student points to it. Its body does not run yet.
- A second rule, a second object. Not a single class is written.
- student(50) passes 50 to the lambda: price is 50, and price * 0.8 gives 40.
- 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;error CS1660: Cannot convert lambda expression to type 'IPriceRule' because it is not a delegate typeThat is why in C# a rule passed as a lambda is a delegate, not an interface.
student(50)is short forstudent.Invoke(50). AndGetType()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));PriceRule40The 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 aT, returns abool. It is a condition: "is the order big?".Func<T, TResult>: takes aT, returns aTResult. The last type in the< >is always the result. It turns one thing into another: an order into a label.Action<T>: takes aT, returns nothing. It does something with it: prints it, saves it.Func<TResult>: takes nothing, returns aTResult. 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);True-> MARIAGuestIn 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));12.67Then 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>andFunc<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");Email: your order has shippedSMS: your order has shippedTry 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));bigAndMaria gives False, and bigOrMaria gives True, because it is Maria's. Georgi's order is €80, it is big, and ! flips the answer: False.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));breadmilkcoffeeRemoved: 2milkTrue5, 4, 6RemoveAll(condition)removes all elements for which the condition is true, and returns how many it removed. The condition is aPredicate.ForEach(action)does the action with each element. It is anAction.Find(condition)returns the first element for which the condition is true, ornullif there is none.Existssays 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:
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
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));mariageorgiELENA43FalsecoffeeThe 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.Containsisname => vips.Contains(name); - a static method:
int.Parseiss => int.Parse(s), andConsole.WriteLineiss => Console.WriteLine(s); - a method of the parameter: there is no short form. Java's
String::toUpperCaseis a lambda in C#,s => s.ToUpper(); - a constructor: no short form either.
ArrayList::newin 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);});Ana 6Ivan 5.5Peter 5.5Boris 4.75b.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
aandbare:
var byTotal = (a, b) => a.Total.CompareTo(b.Total);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,
varcompiles, but it givesFunc<Order, Order, int>. It describes the same thing asComparison<Order>, but it is a different type, andSortdoes not accept it:
var byTotal = (Order a, Order b) => a.Total.CompareTo(b.Total);orders.Sort(byTotal);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
comparingandthenComparingin 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?
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}"));a and b reverses the whole comparison, and it is "by grade, then by name". So both are reversed: the grades go from the highest, and Peter comes before Ivan. If you want only the grades in reverse, you swap a and b only in the comparison of the grades, as in the card before this one.Try it
Find the bug
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.
The lambda reads i, but not at the moment it is written:
ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')The lambda sees the variable i itself, not a copy of its value. Such a lambda is called a closure: it "closes over" the outside variables it uses. The for loop has one single i for all the rounds. The lambdas are called only after the loop, and by then i is already 3. prices[3] does not exist, and the program stops.
The fix is a copy in the body of the loop: int n = i; and n instead of i in the lambda. Each round has its own n, so each lambda remembers its own:
var prices = new List<double> { 12, 75, 40 };var lines = new List<Action>();for (int i = 0; i < prices.Count; i++){ int n = i; lines.Add(() => Console.WriteLine($"{n + 1}. €{prices[n]:F2}"));}lines.ForEach(line => line());1. €12.002. €75.003. €40.00foreach does not have this problem: each round in it has its own variable.
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);127And 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));8050If 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
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.
Accessories | Headphones | €49.90Accessories | Keyboard | €49.90Accessories | Mouse | €25.00Computers | Laptop | €1299.00Computers | Tablet | €399.002. 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.
Sofia, €50 or more: Maria, Sofia: €75.50Outside Sofia: Georgi, Varna: €12.90 Dimitar, Plovdiv: €120.00Free delivery (Sofia or €100 or more): Maria, Sofia: €75.50 Elena, Sofia: €40.00 Dimitar, Plovdiv: €120.00Func<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?€120.00 -> student, then Friday: €88.00; Friday, then student: €90.00€15.00 -> student, then Friday: €0.00; Friday, then student: €0.00More tasks
- Cleaning a list. From a list of emails, remove the empty ones and the ones without
@with oneRemoveAll, then print the rest withForEach(Console.WriteLine). - Students by class. A list of students and their classes (
"10A","10B"). For each class, print the names withFindAlland a lambda that compares the class, andConvertAllthat takes the name. - Your own delegate. Make
delegate bool Validator(string value);and three validators as lambdas: non-empty text, digits only (char.IsDigitfor each character), length up to 10. Try them on a few strings.
Test
Check yourself
Related lessons
Section titled "Related lessons"- Builds on: Exceptions · Lists · Dictionary and HashSet · Nested classes
- Continues in: LINQ · record, switch and patterns · Design patterns · Tests with xUnit
- Tasks on this topic: Modern C#