$ sudo teach IT
Модуль 6 · Цикл for

Урок 6.3 — LINQ и работа с коллекциями

Язык запросов к коллекциям: Select, Where, OrderBy, агрегации и отложенное выполнение.

LINQ (Language Integrated Query) — это встроенный в C# язык запросов к данным. Представь, что твоя коллекция — это маленькая база данных. LINQ позволяет делать с ней то же, что SQL делает с таблицами: выбирать, фильтровать, сортировать, группировать, агрегировать. И всё это — прямо в коде C#, с полной поддержкой IntelliSense и проверкой типов во время компиляции. LINQ появился в .NET Framework 3.5 (2007 год) и навсегда изменил то, как C#-разработчики работают с данными.

🏗️

Что такое LINQ и System.Linq

LINQ — это набор методов расширения (extension methods) для интерфейса IEnumerable<T>. Все коллекции в C# (массивы, списки, словари, множества, строки) реализуют IEnumerable<T>, а значит — поддерживают LINQ.

Чтобы использовать LINQ, нужно подключить пространство имён:

C# · Подключение LINQ
using System.Linq;  // подключает все LINQ-методы

// Теперь доступны методы:
// Where, Select, OrderBy, GroupBy, Count, Sum, Average, Min, Max,
// First, FirstOrDefault, Last, Single, Any, All, Contains,
// ToList, ToArray, ToDictionary, и многие другие

Базовый пример. Допустим, у нас есть список чисел, и мы хотим получить только чётные, отсортировать их по убыванию и удвоить:

C# · LINQ просто
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

var result = numbers
    .Where(n => n % 2 == 0)   // фильтр: только чётные
    .OrderByDescending(n => n) // сортировка по убыванию
    .Select(n => n * 2)        // трансформация: удвоить
    .ToList();                 // в список

Console.WriteLine(string.Join(", ", result)); // 20, 16, 12, 8, 4

// Без LINQ это заняло бы ~10 строк с циклами!
🔍

Фильтрация: Where()

Метод Where() — фильтрует коллекцию по условию. Возвращает новую коллекцию, содержащую только элементы, удовлетворяющие предикату:

C# · Where
List<int> numbers = new() { 5, 12, 8, 21, 3, 17, 10 };

// Числа больше 10
var big = numbers.Where(n => n > 10);
Console.WriteLine(string.Join(", ", big));   // 12, 21, 17

// Чётные числа
var even = numbers.Where(n => n % 2 == 0);
Console.WriteLine(string.Join(", ", even));  // 12, 8, 10

// Строки длиннее 4 символов
string[] names = { "Анна", "Борис", "Вика", "Александр" };
var longNames = names.Where(name => name.Length > 4);
Console.WriteLine(string.Join(", ", longNames)); // Борис, Александр
🔄

Трансформация: Select()

Select() преобразует каждый элемент коллекции по заданному правилу. Как map() в других языках:

C# · Select
int[] numbers = { 1, 2, 3, 4, 5 };

// Удвоить каждый элемент
var doubled = numbers.Select(n => n * 2);
Console.WriteLine(string.Join(", ", doubled));  // 2, 4, 6, 8, 10

// Преобразовать в строки с префиксом
var labeled = numbers.Select(n => $"Число: {n}");
foreach (var item in labeled)
    Console.WriteLine(item);
// Число: 1, Число: 2, ...

// Извлечь одно поле из объектов
List<string> words = new() { "C#", "LINQ", "коллекции" };
var lengths = words.Select(w => w.Length);
Console.WriteLine(string.Join(", ", lengths));  // 2, 4, 9
📊

Сортировка: OrderBy, OrderByDescending

Сортирует коллекцию по ключу. Можно сортировать по одному полю или по нескольким (через ThenBy):

C# · Сортировка
int[] numbers = { 5, 2, 8, 1, 9, 3 };

var sorted = numbers.OrderBy(n => n);
Console.WriteLine(string.Join(", ", sorted));  // 1, 2, 3, 5, 8, 9

var desc = numbers.OrderByDescending(n => n);
Console.WriteLine(string.Join(", ", desc));    // 9, 8, 5, 3, 2, 1

// Сортировка строк по длине
string[] fruits = { "яблоко", "банан", "вишня", "киви" };
var byLength = fruits.OrderBy(f => f.Length);
Console.WriteLine(string.Join(", ", byLength));  // киви, банан, вишня, яблоко

// Сортировка по нескольким полям (ThenBy)
// var people = people.OrderBy(p => p.Age).ThenBy(p => p.Name);
📐

Агрегация: Count, Sum, Average, Min, Max

Эти методы возвращают одно число — результат агрегации по всей коллекции:

C# · Агрегация
int[] numbers = { 10, 20, 30, 40, 50 };

int count = numbers.Count();           // 5
int sum = numbers.Sum();               // 150
double avg = numbers.Average();        // 30
int min = numbers.Min();               // 10
int max = numbers.Max();               // 50

Console.WriteLine($"Count: {count}");
Console.WriteLine($"Sum: {sum}");
Console.WriteLine($"Avg: {avg:F2}");
Console.WriteLine($"Min: {min}, Max: {max}");

// Можно с условием:
int evenCount = numbers.Count(n => n % 2 == 0);  // 3
int bigSum = numbers.Where(n => n > 25).Sum();   // 120 (30+40+50)
double avgEven = numbers.Where(n => n % 2 == 0).Average(); // 33.33...
🎯

Выбор элементов: First, FirstOrDefault, Last, Single

C# · Выбор элементов
List<int> numbers = new() { 10, 20, 30, 40, 50 };

int first = numbers.First();              // 10 — первый элемент
int last = numbers.Last();                // 50 — последний элемент

int firstBig = numbers.First(n => n > 25); // 30 — первый > 25
// Если нет элемента, удовлетворяющего условию — FIRST выбросит InvalidOperationException!
// int none = numbers.First(n => n > 100); // ❌ Ошибка!

int firstSafe = numbers.FirstOrDefault(n => n > 100); // 0 — не нашёл, вернул default(int)
// FirstOrDefault безопаснее — возвращает default(T), если ничего не найдено

int lastSafe = numbers.LastOrDefault(n => n > 100);  // 0 — default

// Single — ровно один элемент (иначе исключение)
int single = numbers.Single(n => n == 30);  // 30 — один такой
// int singleFail = numbers.Single(n => n > 20);  // ❌ Их несколько!

⚠️ First() выбрасывает исключение, если нет подходящих элементов. FirstOrDefault() возвращает default (null для ссылочных, 0 для числовых). Всегда используй *OrDefault, если не уверен на 100% в наличии элемента.

C# · ElementAt
// ElementAt — элемент по индексу (как коллекция[индекс])
int third = numbers.ElementAt(2);           // 30
int thirdSafe = numbers.ElementAtOrDefault(10); // 0 (без ошибки)
✅

Проверки: Any, All, Contains

Эти методы возвращают bool — результат проверки коллекции:

C# · Any, All, Contains
List<int> numbers = new() { 1, 2, 3, 4, 5 };

bool anyEven = numbers.Any(n => n % 2 == 0);    // true — есть чётные
bool allPositive = numbers.All(n => n > 0);      // true — все положительные
bool allBig = numbers.All(n => n > 3);           // false — есть 1,2,3

bool hasThree = numbers.Contains(3);              // true — есть 3

// Any() без условия — проверяет, что коллекция не пуста
bool notEmpty = numbers.Any();                    // true

//Contains — для простой проверки наличия
📦

Группировка: GroupBy

GroupBy() группирует элементы по ключу. Результат — коллекция групп, каждая из которых содержит ключ и элементы этой группы:

C# · GroupBy
string[] words = { "C#", "Java", "Python", "Go", "Rust", "JS", "C" };

// Группировка по длине слова
var groups = words.GroupBy(w => w.Length);

foreach (var group in groups)
{
    Console.WriteLine($"Длина {group.Key}: {string.Join(", ", group)}");
}
// Вывод:
// Длина 2: C#, Go, JS
// Длина 4: Java, Rust
// Длина 6: Python
// Длина 1: C

// Подсчёт элементов в каждой группе
foreach (var group in words.GroupBy(w => w.Length))
{
    Console.WriteLine($"Длина {group.Key}: {group.Count()} слов");
}
🔄

Преобразование: ToList, ToArray, ToDictionary

LINQ-методы возвращают IEnumerable<T> — ленивую последовательность. Чтобы превратить её в конкретную коллекцию, используй:

C# · ToList, ToArray, ToDictionary
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// В список
List<int> evenList = numbers.Where(n => n % 2 == 0).ToList();

// В массив
int[] evenArray = numbers.Where(n => n % 2 == 0).ToArray();

// В словарь (ключ — сам элемент, значение — удвоенный)
Dictionary<int, int> dict = numbers.ToDictionary(n => n, n => n * 2);
foreach (var kvp in dict)
    Console.WriteLine($"{kvp.Key}: {kvp.Value}");
// 1: 2, 2: 4, 3: 6, ...
⏳

Отложенное выполнение (deferred execution)

Важнейшая особенность LINQ: запрос не выполняется в момент создания. Он выполняется только когда ты начинаешь перебирать результат (foreach) или вызываешь агрегирующий метод (ToList, Count, First, Sum и т.д.):

C# · Deferred execution
int[] numbers = { 1, 2, 3, 4, 5 };

// Запрос НЕ выполняется здесь
var bigNumbers = numbers.Where(n => {
    Console.WriteLine($"  Проверяем {n}");
    return n > 3;
});

Console.WriteLine("Запрос создан, но ещё не выполнен!");

// Запрос выполняется ЗДЕСЬ — при переборе
Console.WriteLine("Начинаем перебор:");
foreach (var n in bigNumbers)
{
    Console.WriteLine($"  Результат: {n}");
}

// Вывод:
// Запрос создан, но ещё не выполнен!
// Начинаем перебор:
//   Проверяем 1
//   Проверяем 2
//   Проверяем 3
//   Проверяем 4
//   Результат: 4
//   Проверяем 5
//   Результат: 5

Это означает, что если изменить исходную коллекцию до момента перебора — запрос увидит изменения:

C# · Изменение до перебора
List<int> nums = new() { 1, 2, 3, 4, 5 };

var filtered = nums.Where(n => n > 3);
nums.Add(6);  // изменяем исходную коллекцию
nums.Add(7);

foreach (var n in filtered)
{
    Console.WriteLine(n);  // 4, 5, 6, 7 — 6 и 7 тоже попали!
}
// Потому что перебор происходит ПОСЛЕ добавления
💡 Если нужно «заморозить» результат, вызови ToList() или ToArray() — они выполняют запрос и сохраняют результат.
🔤

Method syntax vs Query syntax

В C# есть два стиля написания LINQ-запросов:

Method syntax (цепочка методов)

То, что мы использовали выше — методы расширения, соединённые точкой. Это наиболее распространённый стиль:

C# · Method syntax
var result = numbers
    .Where(n => n > 5)
    .OrderBy(n => n)
    .Select(n => n * 2)
    .ToList();

Query syntax (похож на SQL)

Более декларативный стиль, напоминающий SQL. Компилятор преобразует его в method syntax:

C# · Query syntax
var result = from n in numbers
             where n > 5
             orderby n
             select n * 2;

// Оба варианта делают одно и то же

Query syntax удобен при группировках и соединениях (Join). Для остального — method syntax короче и привычнее.

🧩

Большой пример — анализ оценок студентов

C# · Анализ с LINQ
using System;
using System.Collections.Generic;
using System.Linq;

class Program
{
    static void Main()
    {
        // Данные студентов
        var students = new[]
        {
            new { Name = "Анна",   Grade = 85, Subject = "Math" },
            new { Name = "Борис",  Grade = 92, Subject = "Physics" },
            new { Name = "Вика",   Grade = 78, Subject = "Math" },
            new { Name = "Глеб",   Grade = 95, Subject = "Chemistry" },
            new { Name = "Дарья",  Grade = 88, Subject = "Physics" },
            new { Name = "Егор",   Grade = 73, Subject = "Math" },
            new { Name = "Жанна",  Grade = 91, Subject = "Chemistry" },
            new { Name = "Захар",  Grade = 84, Subject = "Physics" }
        };
        
        // 1. Все студенты, отсортированные по имени
        Console.WriteLine("1. Студенты по алфавиту:");
        var sorted = students.OrderBy(s => s.Name);
        foreach (var s in sorted)
            Console.WriteLine($"  {s.Name}: {s.Grade} ({s.Subject})");
        
        // 2. Только отличники (оценка >= 90)
        Console.WriteLine("\n2. Отличники:");
        var excellent = students.Where(s => s.Grade >= 90);
        foreach (var s in excellent)
            Console.WriteLine($"  {s.Name}: {s.Grade}");
        
        // 3. Средний балл
        double classAverage = students.Average(s => s.Grade);
        Console.WriteLine($"\n3. Средний балл: {classAverage:F2}");
        
        // 4. Группировка по предметам
        Console.WriteLine("\n4. Группировка по предметам:");
        var bySubject = students.GroupBy(s => s.Subject);
        foreach (var group in bySubject)
        {
            Console.WriteLine($"  {group.Key}:");
            Console.WriteLine($"    Средний: {group.Average(s => s.Grade):F2}");
            Console.WriteLine($"    Макс: {group.Max(s => s.Grade)}");
            Console.WriteLine($"    Мин: {group.Min(s => s.Grade)}");
            Console.WriteLine($"    Студентов: {group.Count()}");
        }
        
        // 5. Топ-3 студента
        Console.WriteLine("\n5. Топ-3 студента:");
        var top3 = students
            .OrderByDescending(s => s.Grade)
            .Take(3);
        foreach (var s in top3)
            Console.WriteLine($"  {s.Name}: {s.Grade}");
        
        // 6. Есть ли кто-то с оценкой ниже 75?
        bool anyLow = students.Any(s => s.Grade < 75);
        Console.WriteLine($"\n6. Есть студенты с оценкой < 75: {anyLow}");
        
        // 7. Проекция — только имена
        Console.WriteLine("\n7. Имена всех студентов:");
        var names = students.Select(s => s.Name);
        Console.WriteLine($"  {string.Join(", ", names)}");
        
        // 8. Отфильтровать и преобразовать
        Console.WriteLine("\n8. Оценки по Physics (в процентах):");
        var physicsGrades = students
            .Where(s => s.Subject == "Physics")
            .Select(s => $"{s.Name}: {s.Grade}%");
        foreach (var g in physicsGrades)
            Console.WriteLine($"  {g}");
    }
}
📊

Сводная таблица LINQ-методов

Метод
Описание
Where
Фильтрация по условию
Select
Преобразование элементов
OrderBy
Сортировка по возрастанию
OrderByDescending
Сортировка по убыванию
ThenBy
Вторичная сортировка
Count
Количество элементов
Sum
Сумма
Average
Среднее арифметическое
Min / Max
Минимум / Максимум
First / FirstOrDefault
Первый элемент (или default)
Last / LastOrDefault
Последний элемент (или default)
Single / SingleOrDefault
Единственный элемент (или default)
Any
Есть ли хотя бы один элемент
All
Все ли удовлетворяют условию
Contains
Есть ли элемент
GroupBy
Группировка по ключу
ToList / ToArray
Преобразование в список/массив
ToDictionary
Преобразование в словарь
Distinct
Уникальные элементы
📌

Что важно запомнить

1️⃣
LINQ — это методы расширения для IEnumerable<T>. Подключается через using System.Linq;. Работает с любыми коллекциями.
2️⃣
Where — фильтрация. Select — трансформация. OrderBy — сортировка. GroupBy — группировка.
3️⃣
Count, Sum, Average, Min, Max — агрегация. Any, All, Contains — проверки (возвращают bool).
4️⃣
First() / Last() — могут выбросить исключение. FirstOrDefault() / LastOrDefault() — безопаснее, возвращают default.
5️⃣
Отложенное выполнение: LINQ-запрос выполняется только при переборе. Чтобы «заморозить» — вызови ToList() или ToArray().
6️⃣
Method syntax (collection.Where(...).Select(...)) используется чаще. Query syntax (from x in ... where ... select) — для сложных запросов.

Тест: 6.3: LINQ и работа с коллекциями

3 вопроса

break и continue

Premium