1. Введение
В реальных проектах почти каждый второй программист тратит добрую часть своей жизни на работу с коллекциями: фильтрует, считает, ищет, пересортировывает, кладёт, достаёт — в общем, ведёт себя с ними почти как с холодильником перед сном. Особенно часто приходится извлекать, обрабатывать и агрегировать данные — будь то списки пользователей, товары в каталоге, строки текста или любые другие массивы.
Почти все современные коллекции в .NET поддерживают функциональные методы — такие как Where, Select, Find, Any, All и прочие. Их сила — в универсальности и лаконичном стиле: вы просто передаёте "кусочек логики" в виде лямбда-выражения, и коллекция оживает, как будто вы завели новый мотор.
LINQ (Language Integrated Query) — это не просто синтаксический сахар, а целый мини-язык внутри C#, позволяющий писать запросы к данным так, будто вы используете SQL или Excel. Только лучше: прямо в коде, с автодополнением, типами и отладчиком.
Но вся эта магия работает благодаря делегатам — а каждый раз писать отдельный метод ради фильтрации массива довольно утомительно. И вот тут лямбда-выражения вступают в игру как мини-функции "на месте", превращая тяжёлый код в элегантный и выразительный.
2. Лямбда-выражения в стандартных методах коллекций
Лямбда-выражения особенно хорошо проявляют себя в стандартных методах коллекций, основанных на делегатах, таких как Find, Exists, ForEach и многих других.
Пример: Поиск по условию
Допустим, у вас есть список товаров:
using System;
using System.Collections.Generic;
// Наш класс товара
public class Product
{
public string Name { get; set; }
public int Price { get; set; }
}
var products = new List<Product>
{
new Product { Name = "Кофе", Price = 100 },
new Product { Name = "Чай", Price = 70 },
new Product { Name = "Молоко", Price = 80 }
};
// Найдём первый дорогой продукт (>90)
Product expensive = products.Find(p => p.Price > 90); // Используем лямбду!
Console.WriteLine(expensive?.Name); // => Кофе
Без лямбды вам бы пришлось писать отдельный метод или анонимную функцию старого стиля. А так — одна строчка, и код читается как английский: "Найди продукт, где цена больше 90".
Пример: Проверка наличия товара
bool hasCheap = products.Exists(p => p.Price < 75);
Console.WriteLine(hasCheap); // => True (ибо "Чай" дешевле 75)
Пример: Обработка всех элементов (ForEach)
Иногда надо что-то сделать с каждым элементом:
products.ForEach(p => Console.WriteLine($"{p.Name}: {p.Price} евро"));
Про аналогии
Если коротко: лямбда-выражения в коллекциях — это как кнопка "сделать красиво" в фоторедакторе. Нажал — и результат!
3. Лямбда-выражения и LINQ: магия для коллекций
LINQ — это не просто удобство, это ещё и отличное вступление в функциональный стиль программирования. Большинство LINQ-методов ожидают на вход делегаты — а значит, их идеальный друг и собутыльник — лямбда-выражения.
Фильтрация с Where
Пусть у нас снова есть список продуктов. Теперь попробуем отобрать только "дешёвые" товары:
using System.Linq;
var cheapProducts = products.Where(p => p.Price < 90);
foreach (var p in cheapProducts)
Console.WriteLine(p.Name); // Чай, Молоко
Получили новую коллекцию, не написав ни одного цикла вручную. Where принимает лямбду-предикат (функцию, возвращающую true/false), и применяет её к каждому элементу.
Сортировка с OrderBy
Кто любит порядок — тому сюда:
var sorted = products.OrderBy(p => p.Price);
foreach (var p in sorted)
Console.WriteLine($"{p.Name}: {p.Price}");
// Чай: 70
// Молоко: 80
// Кофе: 100
Маппинг (Select) – проекция данных
Иногда нам нужно не весь объект, а только его часть, например, список названий товаров:
var names = products.Select(p => p.Name);
foreach (var name in names)
Console.WriteLine(name); // Кофе, Чай, Молоко
Цепочки LINQ
LINQ хорош тем, что можно "цеплять" вызовы один за другим:
var namesOfCheap = products
.Where(p => p.Price < 90)
.OrderBy(p => p.Name)
.Select(p => p.Name.ToUpper());
foreach (var name in namesOfCheap)
Console.WriteLine(name); // МОЛОКО, ЧАЙ
Похоже на цепочку производственных линий: каждый метод — новая стадия обработки.
Вопрос: Чем лямбда-выражения лучше обычных методов для LINQ?
Во-первых, лямбды можно писать прямо в том месте, где они нужны. Во-вторых, лямбда-выражения короткие и читаемые. В-третьих, это стандарт современного C# — так пишут все, а те, кто не пишет, обычно на собеседования не проходят.
4. Практический пример
В течение курса мы писали учебное приложение для работы с небольшим каталогом товаров, пользователей или заказов. Давайте добавим в него современные методы обработки коллекций.
Поиск пользователя по имени
public class User
{
public string Username { get; set; }
public int Age { get; set; }
}
var users = new List<User>
{
new User{ Username = "Alice", Age = 21 },
new User{ Username = "Bob", Age = 26 },
new User{ Username = "Charlie", Age = 32 }
};
// Поиск пользователя по имени
User found = users.FirstOrDefault(u => u.Username == "Bob");
Console.WriteLine(found?.Age); // 26
Фильтрация по возрасту
var adults = users.Where(u => u.Age >= 18);
foreach (var u in adults)
Console.WriteLine(u.Username); // Alice, Bob, Charlie
Подсчёт количества юзеров
int count = users.Count(u => u.Age > 25);
Console.WriteLine(count); // 2 (Bob и Charlie)
Проверка всех юзеров на совершеннолетие
bool allAdults = users.All(u => u.Age >= 18);
Console.WriteLine(allAdults); // True
Есть ли хоть один несовершеннолетний?
bool hasMinor = users.Any(u => u.Age < 18);
Console.WriteLine(hasMinor); // False
5. LINQ: Как это всё работает изнутри
Когда вы пишете, например, Where(u => u.Age > 20), на самом деле это примерно то же, что создать цикл, который перебирает все элементы и проверяет для каждого условие. Только LINQ делает это незаметно и красиво, оборачивая ваш предикат в делегат.
Ведь, если бы не лямбда-выражения, пришлось бы городить такие конструкции:
public static bool AgeMoreThan20(User u) => u.Age > 20;
var adultUsers = users.Where(AgeMoreThan20);
Или вообще анонимные методы в "олдскульном" стиле:
var adultUsers = users.Where(delegate(User u) { return u.Age > 20; });
Всё это громоздко и уныло. С лямбдой — изящно и современно.
6. Делегаты и стандартные типы: Func, Action, Predicate
Не только LINQ обожает лямбда-выражения. Многие методы стандартных коллекций принимают специализированные делегаты, например:
- Predicate<T> — для методов Find, Exists, RemoveAll
- Func<T, TResult> — для LINQ-методов, проекций, вычислений
- Action<T> — для методов, которые делают что-то с элементом, но ничего не возвращают (ForEach)
Вот как это выглядит на практике:
// Predicate<T>
users.RemoveAll(u => u.Age < 30); // Удалили всех моложе 30
// Func<T, TResult>
var names = users.Select(u => u.Username);
// Action<T>
users.ForEach(u => Console.WriteLine(u.Username));
7. Шпаргалка по методам коллекций с лямбда-выражениями
| Метод | Что делает | Тип делегата | Пример лямбды |
|---|---|---|---|
|
Фильтрует элементы | |
|
|
Проецирует, преобразует | |
|
|
Сортировка по ключу | |
|
|
Первый элемент по условию | |
|
|
Есть ли хоть один элемент по условию | |
|
|
Все ли элементы удовлетворяют условию | |
|
|
Кол-во элементов по условию | |
|
|
Для каждого элемента что-то сделать | |
|
|
Удаляет все по предикату | |
|
8. Типичные ошибки и особенности
Одна из самых частых ошибок — забыть, что LINQ не изменяет исходную коллекцию, а возвращает новую последовательность. То есть, после кода var sorted = users.OrderBy(u => u.Age); коллекция users останется в исходном порядке! Это может сбить с толку: иногда кажется, что всё уже отсортировано — а на самом деле нет.
Ещё нюанс: методы типа Where, Select и другие возвращают объекты типа IEnumerable<T>. Это "ленивая" коллекция — реальная обработка начинается, когда вы реально начинаете что-то перечислять (foreach, ToList() и т.д.). Поэтому если хотите материализовать результат, не забудьте вызвать ToList() или ToArray():
var sortedList = users.OrderBy(u => u.Age).ToList();
Также помним: если лямбда-выражение обращается к переменным вне своей области видимости (замыкания), то эти переменные продолжают "жить" в памяти до тех пор, пока жива ссылка на лямбду. Не очень страшно, но если вы используете лямбду внутри долгоживущего объекта и захватили в ней "огромный массив", то массив будет висеть в памяти вместе с лямбдой.
И ещё: используйте говорящие имена параметров и переменных — это сильно повышает читабельность, особенно если у вас несколько уровней вложенных лямбд.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ