1. Advantages of lambda expressions
In programming, similar tasks repeat a lot. For example: "filter a list of users older than 18", "sum all numbers that satisfy a condition", "sort products by price". Without lambdas these tasks used to be solved by creating separate methods — which is extra noise in the code, especially if the action is used only in one place. Lambdas make the code more compact and closer to how we mentally state the task.
Lambda expressions are a standard tool in many modern languages (not just C#), because they let you "pass behavior" as a value, whether it's a filter, a handler or a transformation function.
1. Conciseness and terseness
Lambda expressions let you avoid long declarations of extra methods or anonymous delegates when you write a small piece of functionality "on the fly". For example, here's how a filter of a list of numbers would look before lambdas:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
// Before lambdas:
List<int> evenNumbers = numbers.FindAll(delegate(int x) { return x % 2 == 0; });
// With a lambda expression:
List<int> evenNumbers2 = numbers.FindAll(x => x % 2 == 0);
The result is the same, but the code with the lambda is much more compact. In large projects the line savings become significant.
2. Improved readability and expressiveness
Lambdas let you focus on the essence of the operation, removing syntactic "noise". Your code becomes closer to natural language:
var adults = users.Where(user => user.Age >= 18);
Compare that with declaring a separate method bool IsAdult(User user), which you'd have to write just for this filter.
3. Convenient integration with LINQ and collection APIs
The main power of lambdas is in combination with LINQ and collections. Many standard collection methods and LINQ operators expect a function as a parameter (for example, Func<T, bool> for filtering). A lambda lets you declare the needed function right in place:
var expensive = products.Where(p => p.Price > 1000);
var firstBook = books.FirstOrDefault(b => b.Title.StartsWith("C#"));
var doubled = numbers.Select(n => n * 2);
4. Capturing variables from outer scope (closures)
A lambda expression can use variables declared outside. This gives flexibility and lets you create dynamic functions on the fly:
int minAge = 18;
var filtered = users.Where(u => u.Age >= minAge); // minAge is "captured" by the lambda
This opens interesting patterns for generating functions with "configured parameters".
Fun fact: Inside a lambda you can not only read but sometimes modify outer variables, though you should do that carefully — more on that in the lecture about closures!
5. Inline and contextual code
Lambda expressions "live" where they're used, not scattered across the project among methods. This brings code closer to the principle "maximum information in minimum space".
In the examples as we develop our app (recall, we're building a mini library book accounting system), suppose we had this list of books:
public class Book
{
public string Title { get; set; }
public string Author { get; set; }
public int Year { get; set; }
public double Price { get; set; }
}
// Somewhere in the code:
List<Book> books = new List<Book>
{
new Book { Title = "C# 9.0 in a Nutshell", Author = "Skeet", Year = 2022, Price = 350 },
new Book { Title = "CLR via C#", Author = "Richter", Year = 2019, Price = 250 },
// ...
};
// Find all books more expensive than 300:
var expensiveBooks = books.Where(b => b.Price > 300).ToList();
You see the selection criteria right next to the call, without wasting time searching for external functions in the code.
6. Use as callbacks, events, timers
Lambdas are great for specifying one-off actions, like an event handler (callback):
button.Click += (sender, args) => Console.WriteLine("Button clicked!");
Lambda expressions remove the need to write separate methods when the handler is very simple.
7. Extending delegate capabilities
Previously you had to declare named methods to pass behavior; now you literally write the function in place:
Timer timer = new Timer(_ => Console.WriteLine("Tick!"), null, 0, 1000);
8. Simplifying testing and dependency injection
With lambdas you can easily create fake (mock) behavior implementations for tests, without cluttering the main project with utility classes or temporary classes. For example, if a constructor accepts a delegate, for a test you can pass a lambda with the desired behavior.
2. Main disadvantages of lambda expressions
Like any powerful tool, lambda expressions aren't without faults. Let's discuss what difficulties and limitations they can introduce.
1. Loss of readability with excessive nesting
Lambdas are great until there are too many in one place. Nested lambdas or long lambdas make the code tedious to parse:
var result = items.Select(x => x.Children.Where(y => y.Value > 10)
.Select(z => z.Name.ToUpper())
.ToList());
Add a couple more levels — and hello, "people's puzzle for reading code".
Tip: If a lambda becomes more than 3–4 lines — extract it into a separate named method. Don't be afraid to look old-fashioned: readability is more important than fashionable shortcuts.
2. Debugging difficulties
Lambdas are not very debugger-friendly, especially if they're written "in one line" directly in a LINQ chain. Sometimes it's hard to set a breakpoint inside a lambda or inspect variable values at a specific stage.
To simplify debugging, you can temporarily move the lambda body to a named method or split long LINQ chains into sections with intermediate variables.
3. Non-obvious argument and return types
A lambda expression is often passed as a delegate (Func<...>, Action<...>, Predicate<T>). Sometimes it's hard to immediately understand what the exact types of input parameters and the return type should be, especially in methods with generic parameters.
For example:
Func<int, string, double> myFunc = (a, b) => a + b.Length; // Oops! An int will be returned, but double was expected.
The compiler will point out the error, but a beginner might not immediately see that the lambda "doesn't fit the shape".
4. Problems with variable capture
Capturing variables from the outer scope (closure) is a double-edged sword. If you use captured variables carelessly, you can get unexpected results. For example, in a loop:
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var action in actions) action();
Many expect the output 0 1 2, but we get 3 3 3. Why? By the time the lambda runs, the variable i is already 3! The lambda captured the variable itself, not its value.
This is a typical mistake for beginners, covered in detail in the official documentation. It can be solved — but requires caution.
5. Loss of explicit names and reuse problems
Lambda expressions are good for one-off actions. But if the same condition/function is used in several places, it's better to extract the logic into a named method. Otherwise you risk duplication and mistakes when making changes.
6. Inconvenience adding XML comments
You can't attach XML documentation to a lambda for auto-generated docs (like you can for methods). Comments for lambdas have to be regular inline comments in the code.
7. Possible performance issues
In most cases lambdas aren't noticeably slower than regular methods. However, when you frequently and massively create lambdas that capture variables they lead to allocations of additional objects (closures). In performance-critical spots (for example, in a tight loop or high-load services) consider whether using static methods might be cheaper.
8. Can't use goto, break, continue relative to outer loops
If a lambda is declared inside a loop, you can't directly use break or continue inside it to affect the outer loop — that's syntactically not allowed.
9. Lambdas can't describe all behavior
Lambdas can't directly work with attributes, you can't specify access modifiers, and you can't do some special things — for example, declare named local functions with attributes.
3. Choosing between the two evils
When lambda expressions are useful
| Scenario | Is lambda convenient? | Why |
|---|---|---|
| Short filtering/transformation | 👍 | Quick and clear |
| Multi-level nested operations | 👎 | Becomes unreadable |
| Re-use (repeated usage) | 👎 | Better to extract to a method |
| Callback logic, events | 👍 | Compact |
| Describing complex business logic | 👎 | Name + comments are needed |
| Working with LINQ | 👍 | Ideal scenario |
When it's better to avoid lambdas
- If the logic is long and contains many branches/calculations.
- If the lambda does something non-obvious to the reader and lacks explanation.
- If the function needs documentation, is used in multiple places, or needs a "speaking" name.
- If the lambda is too deep in nested calls — there's a risk of losing readability.
4. Common mistakes when working with lambda expressions
Variable capture mistake in a loop:
List<Action> actions = new List<Action>();
for (int i = 0; i < 5; i++)
{
actions.Add(() => Console.WriteLine(i));
}
foreach (var act in actions) act(); // They'll all print 5!
How to do it correctly:
for (int i = 0; i < 5; i++)
{
int captured = i; // capture a separate variable
actions.Add(() => Console.WriteLine(captured));
}
Too long a lambda:
books.Where(b => b.Price > 1000 && b.Title.Contains("C#") && b.Author.Length > 4 && bla-bla-bla...);
// The code became unreadable, extract to a method!
Using a lambda where a documented method is needed:
If the function is used many times or must be thoroughly commented, write a named method:
bool IsExpensiveBook(Book book) => book.Price > 1000;
books.Where(IsExpensiveBook);
GO TO FULL VERSION