JavaRush /Курси /C# SELF /Лямбда-вирази в колекціях і

Лямбда-вирази в колекціях і LINQ

C# SELF
Рівень 49 , Лекція 2
Відкрита

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. Шпаргалка по методах колекцій з лямбда-виразами

Метод Що робить Тип делегата Приклад лямбди
Where
Фільтрує елементи
Func<T, bool>
p => p.Price > 100
Select
Проєктує, перетворює
Func<T, U>
p => p.Name
OrderBy
Сортування за ключем
Func<T, K>
u => u.Age
FirstOrDefault
Перший елемент за умовою
Func<T, bool>
u => u.Username == "Bob"
Any
Чи є хоч один елемент за умовою
Func<T, bool>
u => u.Age < 18
All
Чи всі елементи задовольняють умову
Func<T, bool>
u => u.Age >= 18
Count
Кількість елементів за умовою
Func<T, bool>
p => p.Price > 50
ForEach
Виконати дію для кожного елемента
Action<T>
u => Console.WriteLine(u.Name)
RemoveAll
Видаляє всі елементи за предикатом
Predicate<T>
u => u.Age < 18

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();

Також варто пам’ятати: якщо лямбда-вираз звертається до змінних поза своєю зоною видимості (замикання), то ці змінні продовжують «жити» в пам’яті доти, доки живе посилання на лямбду. Нічого критичного, але якщо ви використовуєте лямбду всередині довгоживучого об’єкта й захопили в ній «величезний масив», то масив висітиме в пам’яті разом із лямбдою.

І ще: використовуйте промовисті назви параметрів і змінних — це істотно підвищує читабельність, особливо якщо маєте кілька рівнів вкладених лямбд.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ