Урок 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, нужно подключить пространство имён:
using System.Linq; // подключает все LINQ-методы
// Теперь доступны методы:
// Where, Select, OrderBy, GroupBy, Count, Sum, Average, Min, Max,
// First, FirstOrDefault, Last, Single, Any, All, Contains,
// ToList, ToArray, ToDictionary, и многие другие
Базовый пример. Допустим, у нас есть список чисел, и мы хотим получить только чётные, отсортировать их по убыванию и удвоить:
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() — фильтрует коллекцию по условию. Возвращает новую коллекцию, содержащую только элементы, удовлетворяющие предикату:
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() в других языках:
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):
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
Эти методы возвращают одно число — результат агрегации по всей коллекции:
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
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% в наличии элемента.
// ElementAt — элемент по индексу (как коллекция[индекс])
int third = numbers.ElementAt(2); // 30
int thirdSafe = numbers.ElementAtOrDefault(10); // 0 (без ошибки)
Проверки: Any, All, Contains
Эти методы возвращают bool — результат проверки коллекции:
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() группирует элементы по ключу. Результат — коллекция групп, каждая из которых содержит ключ и элементы этой группы:
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> — ленивую последовательность. Чтобы превратить её в конкретную коллекцию, используй:
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 и т.д.):
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
Это означает, что если изменить исходную коллекцию до момента перебора — запрос увидит изменения:
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 (цепочка методов)
То, что мы использовали выше — методы расширения, соединённые точкой. Это наиболее распространённый стиль:
var result = numbers
.Where(n => n > 5)
.OrderBy(n => n)
.Select(n => n * 2)
.ToList();
Query syntax (похож на SQL)
Более декларативный стиль, напоминающий SQL. Компилятор преобразует его в method syntax:
var result = from n in numbers
where n > 5
orderby n
select n * 2;
// Оба варианта делают одно и то же
Query syntax удобен при группировках и соединениях (Join). Для остального — method syntax короче и привычнее.
Большой пример — анализ оценок студентов
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-методов
Что важно запомнить
using System.Linq;. Работает с любыми коллекциями.ToList() или ToArray().collection.Where(...).Select(...)) используется чаще. Query syntax (from x in ... where ... select) — для сложных запросов.Тест: 6.3: LINQ и работа с коллекциями
3 вопроса