Урок 8.4 — Лямбда-функции
Анонимные функции в одну строку. Func, Action и замыкания.
Представь, что тебе нужно написать функцию, которая используется всего один раз. Лень создавать отдельный метод, давать ему имя, прописывать все формальности? Лямбда-функция — это анонимный метод, который ты пишешь прямо на месте использования. Как стикер, на котором ты набросал заметку, не заводя для неё отдельный блокнот.
Синтаксис лямбда-выражений
Лямбда — это краткая запись анонимного метода. Базовый синтаксис:
(параметры) => выражение
Символ => читается как «стрелка» или «переходит в». Слева — параметры, справа — тело функции.
// Самая простая лямбда: принимает int, возвращает int
Func<int, int> square = x => x * x;
Console.WriteLine(square(5)); // 25
// Лямбда с двумя параметрами
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(3, 7)); // 10
// Лямбда без параметров
Func<int> getRandom = () => new Random().Next(1, 100);
Console.WriteLine(getRandom());
// Лямбда с одним параметром — скобки можно опустить
Func<int, int> triple = x => x * 3;
Console.WriteLine(triple(10)); // 30
// Типы параметров можно указывать явно:
Func<int, int> doubler = (int x) => x * 2;
Лямбда-выражение vs Лямбда-инструкция
Лямбда-выражение имеет только одну строку справа от =>, без фигурных скобок и без return. Лямбда-инструкция использует фигурные скобки и может содержать несколько строк:
// Лямбда-ВЫРАЖЕНИЕ — одна строка, без return
Func<int, int, int> multiply = (a, b) => a * b;
// Лямбда-ИНСТРУКЦИЯ — несколько строк, с return
Func<int, int, int> complex = (a, b) =>
{
Console.WriteLine($"Считаем: {a} * {b}");
int result = a * b;
Console.WriteLine($"Результат: {result}");
return result;
};
Console.WriteLine(multiply(5, 3)); // 15 (лямбда-выражение)
Console.WriteLine(complex(5, 3)); // с логами + 15 (лямбда-инструкция)
Делегаты Func и Action
Лямбды работают через делегаты — типы, которые описывают сигнатуру метода. В .NET есть готовые делегаты:
ActionAction<T1>Action<T1, T2>Func<TResult>Func<T1, TResult>Func<T1, T2, TResult>// Action — ничего не возвращает
Action greet = () => Console.WriteLine("Привет!");
greet(); // Привет!
Action<string> greetUser = name => Console.WriteLine($"Привет, {name}!");
greetUser("Анна"); // Привет, Анна!
Action<string, int> repeat = (text, count) =>
{
for (int i = 0; i < count; i++)
Console.WriteLine(text);
};
repeat("C#", 3);
// Func — возвращает значение
Func<int, int, int> max = (a, b) => a > b ? a : b;
Console.WriteLine(max(10, 7)); // 10
// Можно передавать лямбду в метод:
static void ProcessNumbers(int[] numbers, Func<int, int> processor)
{
for (int i = 0; i < numbers.Length; i++)
numbers[i] = processor(numbers[i]);
}
int[] data = { 1, 2, 3, 4 };
ProcessNumbers(data, n => n * n);
Console.WriteLine(string.Join(", ", data)); // 1, 4, 9, 16
Лямбды с LINQ — самое частое применение
На практике лямбды чаще всего используются с LINQ — Language Integrated Query. LINQ-методы принимают лямбды для фильтрации, проекции, сортировки данных:
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// Where — фильтрация (лямбда возвращает bool)
var evens = numbers.Where(n => n % 2 == 0);
Console.WriteLine("Чётные: " + string.Join(", ", evens));
// Select — преобразование (проекция)
var squares = numbers.Select(x => x * x);
Console.WriteLine("Квадраты: " + string.Join(", ", squares));
// OrderBy — сортировка по ключу
var sorted = numbers.OrderByDescending(x => x);
Console.WriteLine("По убыванию: " + string.Join(", ", sorted));
// Все вместе:
var result = numbers
.Where(n => n > 3) // оставляем > 3
.Select(n => n * 10) // умножаем на 10
.OrderBy(n => n); // сортируем
Console.WriteLine("Результат: " + string.Join(", ", result));
// Any, All, Count — проверки
bool hasBig = numbers.Any(n => n > 8);
bool allPositive = numbers.All(n => n > 0);
int countEven = numbers.Count(n => n % 2 == 0);
Console.WriteLine($"Есть > 8: {hasBig}"); // True
Console.WriteLine($"Все > 0: {allPositive}"); // True
Console.WriteLine($"Чётных: {countEven}"); // 5
// First, FirstOrDefault
int firstBig = numbers.First(n => n > 5);
int firstOr = numbers.FirstOrDefault(n => n > 20);
Console.WriteLine($"Первое > 5: {firstBig}"); // 6
Console.WriteLine($"Первое > 20: {firstOr}"); // 0 (default)
}
}
System.Linq. Добавь using System.Linq; в начале файла. Лямбды в LINQ — это стандарт де-факто.Замыкания (Closures)
Лямбда может «захватывать» переменные из внешнего контекста. Это называется замыкание (closure). Лямбда «запоминает» окружение, в котором была создана, и может использовать его переменные:
int multiplier = 3; // внешняя переменная
Func<int, int> multiplyBy = x => x * multiplier;
// лямбда захватила multiplier ↑
Console.WriteLine(multiplyBy(5)); // 15
Console.WriteLine(multiplyBy(10)); // 30
// Если изменить внешнюю переменную:
multiplier = 10;
Console.WriteLine(multiplyBy(5)); // 50 — лямбда видит новое значение!
// Замыкание с изменяемой коллекцией:
static List<Func<int>> CreateCounters()
{
var counters = new List<Func<int>>();
for (int i = 0; i < 5; i++)
{
// ⚠️ Внимание: захватывается одна и та же переменная i!
counters.Add(() => i);
}
return counters;
}
var counters = CreateCounters();
foreach (var c in counters)
Console.Write(c() + " "); // 5 5 5 5 5 (а не 0 1 2 3 4!)
Console.WriteLine();
// Как исправить — копировать в локальную переменную:
static List<Func<int>> CreateCountersFixed()
{
var counters = new List<Func<int>>();
for (int i = 0; i < 5; i++)
{
int copy = i; // копируем в локальную переменную цикла
counters.Add(() => copy);
}
return counters;
}
var fixedCounters = CreateCountersFixed();
foreach (var c in fixedCounters)
Console.Write(c() + " "); // 0 1 2 3 4
Console.WriteLine();
⚠️ Важно: в C# 5+ замыкание на переменную цикла for работает корректно (каждая итерация создаёт новую переменную). Но в старых версиях (до C# 5) — была та же проблема, что показана выше. Если пишешь код, совместимый со старыми версиями — всегда копируй в локальную переменную.
Ещё пример замыкания — «фабрика» функций:
static Func<int, int> CreateAdder(int increment)
{
return x => x + increment; // increment захвачен
}
var add5 = CreateAdder(5);
var add10 = CreateAdder(10);
Console.WriteLine(add5(3)); // 8
Console.WriteLine(add5(20)); // 25
Console.WriteLine(add10(3)); // 13
// Каждая лямбда хранит свой increment:
// add5 помнит increment = 5
// add10 помнит increment = 10
Когда использовать лямбды?
✅ Хорошие случаи:
- Короткая логика на месте —
Where(x => x > 5) - Обратные вызовы (callbacks) — обработчики событий, OnClick
- Передача поведения в метод — «как обработать каждый элемент»
- LINQ-запросы — фильтрация, сортировка, группировка
- Фабрики функций — создание функций с настроенным поведением
❌ Плохие случаи:
- Сложная многострочная логика — лучше создать отдельный метод с именем
- Повторяющееся выражение — не копируй одну лямбду 5 раз, вынеси в метод
- Рекурсия — лямбды сложно делать рекурсивными
- Читаемость страдает — если от一行 лямбды глаза разбегаются, пора сделать метод
Практический пример: работа с лямбдами
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var students = new List<(string Name, int Age, double Grade)>
{
("Анна", 20, 4.5),
("Борис", 22, 3.8),
("Вика", 19, 5.0),
("Глеб", 21, 4.0),
("Дарья", 23, 4.8),
};
// Фильтрация: отличники (оценка >= 4.5)
var excellent = students.Where(s => s.Grade >= 4.5);
Console.WriteLine("Отличники:");
foreach (var s in excellent)
Console.WriteLine($" {s.Name} — {s.Grade}");
// Проекция: только имена
var names = students.Select(s => s.Name);
Console.WriteLine("Имена: " + string.Join(", ", names));
// Сортировка по возрасту
var byAge = students.OrderBy(s => s.Age);
Console.WriteLine("\nПо возрасту:");
foreach (var s in byAge)
Console.WriteLine($" {s.Name} — {s.Age} лет");
// Агрегация: средняя оценка
double avgGrade = students.Average(s => s.Grade);
Console.WriteLine($"\nСредняя оценка: {avgGrade:F2}");
// Комбинация: имена отличников, отсортированные по возрасту
var topStudents = students
.Where(s => s.Grade >= 4.5)
.OrderByDescending(s => s.Grade)
.Select(s => $"{s.Name} ({s.Grade})");
Console.WriteLine("\nЛучшие студенты:");
foreach (var s in topStudents)
Console.WriteLine($" {s}");
// Создание кастомной функции через замыкание
double minGrade = 4.0;
var filtered = students.Where(s => s.Grade >= minGrade);
Console.WriteLine($"\nСтуденты с оценкой >= {minGrade}:");
foreach (var s in filtered)
Console.WriteLine($" {s.Name} — {s.Grade}");
}
}
Мини-тест
Вопрос 1
Что выведет код: Func<int, int> f = x => x + 1; Console.WriteLine(f(5));?
Ответ: 6. Лямбда просто прибавляет 1 к аргументу.
Вопрос 2
В чём разница между лямбда-выражением и лямбда-инструкцией?
Ответ: Выражение — одна строка без return и {}. Инструкция — несколько строк с return и { }.
Вопрос 3
Что такое замыкание (closure) в контексте лямбд?
Ответ: Когда лямбда «захватывает» переменные из внешнего контекста и может их использовать (и даже изменять).
Вопрос 4
Какой делегат использовать, если метод возвращает int и принимает два double?
Ответ: Func<double, double, int>. Последний тип в Func — возвращаемый, остальные — параметры.
Задание для тренировки
Напиши программу «Анализ продаж», которая:
- Создаёт список кортежей
(string Product, double Price, int Qty)— 6 продаж - Использует LINQ с лямбдами для:
- Выбора товаров дороже 1000
- Сортировки по сумме (Price * Qty)
- Группировки по цене (дешёвые < 500, средние 500-2000, дорогие > 2000)
- Вычисления общей выручки
- Создаёт функцию через замыкание, которая фильтрует товары с прибылью выше заданного порога
- Выводит результаты
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
var sales = new List<(string Product, double Price, int Qty)>
{
("Ноутбук", 45000, 2),
("Мышь", 1500, 10),
("Клавиатура", 2500, 5),
("Наушники", 3500, 3),
("Кабель USB", 300, 20),
("Монитор", 22000, 1),
};
// Товары дороже 1000
var expensive = sales.Where(s => s.Price > 1000)
.Select(s => $"{s.Product} — {s.Price} руб.");
Console.WriteLine("Дороже 1000:");
foreach (var s in expensive)
Console.WriteLine($" {s}");
// Сортировка по сумме (цена * количество)
var byTotal = sales
.OrderByDescending(s => s.Price * s.Qty)
.Select(s => $"{s.Product}: {s.Price * s.Qty} руб.");
Console.WriteLine("\nПо сумме продаж:");
foreach (var s in byTotal)
Console.WriteLine($" {s}");
// Группировка по ценовой категории
var groups = sales.GroupBy(s => s.Price switch
{
< 500 => "Дешёвые",
<= 2000 => "Средние",
_ => "Дорогие"
});
Console.WriteLine("\nПо категориям:");
foreach (var group in groups)
{
Console.WriteLine($" {group.Key}: {group.Count()} товаров");
foreach (var s in group)
Console.WriteLine($" - {s.Product} ({s.Price} руб.)");
}
// Общая выручка
double totalRevenue = sales.Sum(s => s.Price * s.Qty);
Console.WriteLine($"\nОбщая выручка: {totalRevenue:N0} руб.");
// Замыкание — фильтр по порогу прибыли
double profitThreshold = 5000;
Func<(string Product, double Price, int Qty), bool>
isProfitable = s => s.Price * s.Qty >= profitThreshold;
var profitable = sales.Where(isProfitable);
Console.WriteLine($"\nТовары с выручкой >= {profitThreshold}:");
foreach (var s in profitable)
Console.WriteLine($" {s.Product} — {s.Price * s.Qty:N0} руб.");
}
}
Что важно запомнить
(параметры) => выражение или (параметры) => { инструкции; return; }.Func<T1, T2, TResult> — два параметра, возвращает TResult.Where(x => условие), Select(x => преобразование), OrderBy(x => ключ).Тест: 8.4: Лямбда-функции
3 вопроса