🔷 C#

LINQ في C#

LINQ в C#: работа с коллекциями как с базой данных

Представь, что у тебя есть список из 10 000 студентов в программе. Тебе нужно найти всех отличников старше 18 лет, отсортировать их по фамилии и взять первых 10. На обычных циклах это несколько строк кода. С LINQ — одна строка, читаемая как обычный текст. LINQ (Language Integrated Query) — это суперсила C# для работы с данными!

Что такое LINQ

LINQ — это встроенный в C# язык запросов к данным. Он позволяет обрабатывать коллекции (списки, массивы, базы данных, XML) одинаковым способом. Как если бы ты мог делать SQL-запросы прямо в коде C#!

using System.Linq; // Подключаем LINQ

// Наши данные — список студентов
var students = new List
{
    new Student { Name = "Алиса", Age = 20, Grade = 95, City = "Москва" },
    new Student { Name = "Борис", Age = 17, Grade = 72, City = "СПб" },
    new Student { Name = "Вера", Age = 22, Grade = 88, City = "Москва" },
    new Student { Name = "Григорий", Age = 19, Grade = 91, City = "Казань" },
    new Student { Name = "Дарья", Age = 21, Grade = 65, City = "Москва" },
    new Student { Name = "Егор", Age = 18, Grade = 97, City = "СПб" },
};

record Student { public string Name; public int Age; public int Grade; public string City; }

Два синтаксиса LINQ: методный и query

// Найти всех с оценкой >= 90

// МЕТОДНЫЙ синтаксис (чаще используется)
var topStudents = students
    .Where(s => s.Grade >= 90)
    .ToList();

// QUERY синтаксис (похож на SQL)
var topStudents2 = 
    from s in students
    where s.Grade >= 90
    select s;

// Результат одинаковый! Используй тот, который удобнее читать.

WHERE: фильтрация (как SQL WHERE)

// Простой фильтр
var adults = students.Where(s => s.Age >= 18);

// Несколько условий
var goodAdults = students.Where(s => s.Age >= 18 && s.Grade >= 80);

// Фильтр строк
var moscowStudents = students.Where(s => s.City == "Москва");
var studentsWithA = students.Where(s => s.Name.StartsWith("А"));

// Вывод результата
foreach (var s in goodAdults)
{
    Console.WriteLine($"{s.Name}: {s.Age} лет, {s.Grade} баллов");
}
// Алиса: 20 лет, 95 баллов
// Вера: 22 лет, 88 баллов
// Григорий: 19 лет, 91 баллов
// Егор: 18 лет, 97 баллов

SELECT: преобразование (как SQL SELECT)

// Выбрать только имена
var names = students.Select(s => s.Name).ToList();
Console.WriteLine(string.Join(", ", names));
// Алиса, Борис, Вера, Григорий, Дарья, Егор

// Преобразование в новый тип
var nameGrades = students.Select(s => new { s.Name, s.Grade }).ToList();
foreach (var item in nameGrades)
{
    Console.WriteLine($"{item.Name}: {item.Grade}");
}

// Вычисления в Select
var gradeLetters = students.Select(s => new 
{
    s.Name,
    Letter = s.Grade >= 90 ? "A" : s.Grade >= 80 ? "B" : s.Grade >= 70 ? "C" : "F"
}).ToList();

// SelectMany — разворачивает вложенные коллекции
var courses = new[]
{
    new { Student = "Алиса", Subjects = new[] { "Математика", "Физика" } },
    new { Student = "Борис", Subjects = new[] { "История", "Литература", "Химия" } }
};

var allSubjects = courses.SelectMany(c => c.Subjects).ToList();
Console.WriteLine(string.Join(", ", allSubjects));
// Математика, Физика, История, Литература, Химия

ORDER BY: сортировка

// По возрастанию
var byAge = students.OrderBy(s => s.Age).ToList();

// По убыванию
var byGradeDesc = students.OrderByDescending(s => s.Grade).ToList();

// Сортировка по нескольким полям
var sorted = students
    .OrderBy(s => s.City)          // Сначала по городу
    .ThenByDescending(s => s.Grade) // Затем по оценке (убыв.)
    .ToList();

foreach (var s in sorted)
{
    Console.WriteLine($"{s.City} | {s.Name}: {s.Grade}");
}
// Казань  | Григорий: 91
// Москва  | Алиса: 95
// Москва  | Вера: 88
// Москва  | Дарья: 65
// СПб     | Егор: 97
// СПб     | Борис: 72

GROUP BY: группировка

// Сгруппировать студентов по городам
var byCity = students.GroupBy(s => s.City);

foreach (var group in byCity)
{
    Console.WriteLine($"\nГород: {group.Key} ({group.Count()} студентов)");
    foreach (var s in group)
    {
        Console.WriteLine($"  {s.Name}: {s.Grade}");
    }
}

// Статистика по группам
var cityStats = students
    .GroupBy(s => s.City)
    .Select(g => new 
    {
        City = g.Key,
        Count = g.Count(),
        AvgGrade = g.Average(s => s.Grade),
        BestStudent = g.OrderByDescending(s => s.Grade).First().Name
    })
    .OrderByDescending(g => g.AvgGrade)
    .ToList();

foreach (var stat in cityStats)
{
    Console.WriteLine($"{stat.City}: {stat.Count} чел., средний балл: {stat.AvgGrade:F1}, лучший: {stat.BestStudent}");
}
// СПб: 2 чел., средний балл: 84.5, лучший: Егор
// Москва: 3 чел., средний балл: 82.7, лучший: Алиса
// Казань: 1 чел., средний балл: 91.0, лучший: Григорий

JOIN: объединение коллекций

// Данные
var studentsList = new[]
{
    new { Id = 1, Name = "Алиса", CourseId = 101 },
    new { Id = 2, Name = "Борис", CourseId = 102 },
    new { Id = 3, Name = "Вера", CourseId = 101 },
};

var courses2 = new[]
{
    new { Id = 101, CourseName = "Программирование" },
    new { Id = 102, CourseName = "Дизайн" },
    new { Id = 103, CourseName = "Маркетинг" },
};

// JOIN — как SQL INNER JOIN
var studentCourses = studentsList
    .Join(
        courses2,
        s => s.CourseId,      // Ключ из studentsList
        c => c.Id,             // Ключ из courses2
        (s, c) => new { s.Name, c.CourseName } // Результат
    )
    .ToList();

foreach (var item in studentCourses)
{
    Console.WriteLine($"{item.Name} → {item.CourseName}");
}
// Алиса → Программирование
// Борис → Дизайн
// Вера → Программирование

Агрегатные функции

var grades = students.Select(s => s.Grade).ToList();

Console.WriteLine($"Количество: {students.Count()}");
Console.WriteLine($"Средний балл: {students.Average(s => s.Grade):F2}");
Console.WriteLine($"Максимальный: {students.Max(s => s.Grade)}");
Console.WriteLine($"Минимальный: {students.Min(s => s.Grade)}");
Console.WriteLine($"Сумма баллов: {students.Sum(s => s.Grade)}");

// Проверки
bool anyExcellent = students.Any(s => s.Grade >= 95);     // Есть хоть один?
bool allPassed = students.All(s => s.Grade >= 60);         // Все сдали?
int failCount = students.Count(s => s.Grade < 70);         // Сколько не сдали?

// Поиск
var best = students.MaxBy(s => s.Grade);    // Студент с макс. оценкой
var worst = students.MinBy(s => s.Grade);   // Студент с мин. оценкой
var first = students.First(s => s.City == "Москва");       // Первый из Москвы
var firstOrNull = students.FirstOrDefault(s => s.Age > 100); // null если не найден

IEnumerable: ленивые вычисления

// LINQ операции ленивые (lazy) — они не выполняются сразу!
var query = students.Where(s => s.Grade >= 80); // Ничего не вычисляется!

// Вычисление происходит при:
// 1. Перечислении (foreach)
foreach (var s in query) { } // Вот тут!

// 2. Вызове ToList(), ToArray() и т.д.
var list = query.ToList(); // Выполняется и сохраняется

// 3. Вызове агрегатных функций
int count = query.Count(); // Выполняется

// Почему это важно?
var numbers = Enumerable.Range(1, 1000000); // Миллион чисел

// МЕДЛЕННО: сначала фильтруем все миллион, потом берём 5
var slow = numbers.Where(n => n % 2 == 0).ToList().Take(5);

// БЫСТРО: берём 5 из ленивой последовательности (считает только нужные)
var fast = numbers.Where(n => n % 2 == 0).Take(5).ToList();

Async/Await: асинхронное программирование

Когда программа ждёт ответа от сервера, базы данных или файловой системы — она может заморозиться. Async/await позволяет ждать не блокируя поток!

using System.Threading.Tasks;
using System.Net.Http;

// Синхронный метод — блокирует поток
static string GetDataSync(string url)
{
    // Пока данные загружаются — ничего не работает
    using var client = new HttpClient();
    return client.GetStringAsync(url).Result; // Блокировка!
}

// Асинхронный метод — не блокирует
static async Task GetDataAsync(string url)
{
    using var client = new HttpClient();
    string result = await client.GetStringAsync(url); // Ждём без блокировки
    return result;
}

// Асинхронный Main
static async Task Main()
{
    Console.WriteLine("Начинаем загрузку...");
    
    // await — ждём завершения Task не блокируя поток
    string data = await GetDataAsync("https://api.example.com/data");
    Console.WriteLine($"Получено: {data.Length} символов");
    
    Console.WriteLine("Программа продолжает работу!");
}
// Task — обёртка для асинхронной операции
// Task — как Task, ничего не возвращает
// Task — возвращает значение типа T

static async Task DelayedMessage(string msg, int delayMs)
{
    await Task.Delay(delayMs); // Ждём N миллисекунд
    Console.WriteLine(msg);
}

// Запуск нескольких задач параллельно
static async Task RunParallel()
{
    var task1 = DelayedMessage("Задача 1 готова", 1000);
    var task2 = DelayedMessage("Задача 2 готова", 500);
    var task3 = DelayedMessage("Задача 3 готова", 750);
    
    // Ждём завершения ВСЕХ задач
    await Task.WhenAll(task1, task2, task3);
    Console.WriteLine("Все задачи завершены!");
    
    // Ждём завершения ЛЮБОЙ задачи
    // await Task.WhenAny(task1, task2, task3);
}

Итого

  • ✅ LINQ — язык запросов к коллекциям, встроенный в C#
  • ✅ Where — фильтрация (как SQL WHERE)
  • ✅ Select — преобразование (как SQL SELECT)
  • ✅ OrderBy/ThenBy — сортировка
  • ✅ GroupBy — группировка
  • ✅ Join — объединение коллекций
  • ✅ Count/Sum/Average/Min/Max — агрегатные функции
  • ✅ IEnumerable — ленивые вычисления, выполняются при переборе
  • ✅ async/await — асинхронное программирование без блокировки
  • ✅ Task/Task<T> — обёртка для асинхронной операции

LINQ и работа с файлами

// Практический пример: анализ данных из файла (CSV)
// Допустим, у нас есть данные о продажах

var salesData = new[]
{
    (Month: "Январь",  Product: "Ноутбук",  Units: 15, Price: 89999m),
    (Month: "Январь",  Product: "Мышь",     Units: 150, Price: 1299m),
    (Month: "Февраль", Product: "Ноутбук",  Units: 18, Price: 89999m),
    (Month: "Февраль", Product: "Наушники", Units: 45, Price: 8999m),
    (Month: "Март",    Product: "Ноутбук",  Units: 22, Price: 89999m),
    (Month: "Март",    Product: "Мышь",     Units: 200, Price: 1299m),
    (Month: "Март",    Product: "Наушники", Units: 60, Price: 8999m),
};

// Общая выручка за все месяцы
decimal totalRevenue = salesData.Sum(s => s.Units * s.Price);
Console.WriteLine($"Общая выручка: {totalRevenue:C}");

// Выручка по месяцам
var byMonth = salesData
    .GroupBy(s => s.Month)
    .Select(g => new {
        Month = g.Key,
        Revenue = g.Sum(s => s.Units * s.Price),
        Orders = g.Count()
    })
    .OrderBy(m => Array.IndexOf(new[] {"Январь","Февраль","Март"}, m.Month));

Console.WriteLine("\nВыручка по месяцам:");
foreach (var m in byMonth)
    Console.WriteLine($"  {m.Month,-10}: {m.Revenue,12:C} ({m.Orders} позиций)");

// Топ продуктов
var topProducts = salesData
    .GroupBy(s => s.Product)
    .Select(g => new {
        Product = g.Key,
        TotalUnits = g.Sum(s => s.Units),
        TotalRevenue = g.Sum(s => s.Units * s.Price)
    })
    .OrderByDescending(p => p.TotalRevenue)
    .ToList();

Console.WriteLine("\nТоп продуктов по выручке:");
for (int i = 0; i < topProducts.Count; i++)
{
    var p = topProducts[i];
    Console.WriteLine($"  {i+1}. {p.Product}: {p.TotalUnits} шт, {p.TotalRevenue:C}");
}

Parallel LINQ (PLINQ): многопоточная обработка

using System.Linq;

// Обычный LINQ (последовательный)
var numbers = Enumerable.Range(1, 10_000_000);

var sw = System.Diagnostics.Stopwatch.StartNew();
var sumSeq = numbers.Where(n => n % 2 == 0).Sum();
sw.Stop();
Console.WriteLine($"Последовательно: {sumSeq:N0} за {sw.ElapsedMilliseconds}ms");

// PLINQ (параллельный) — автоматически использует все ядра!
sw.Restart();
var sumPar = numbers.AsParallel()         // Включаем параллельность
    .Where(n => n % 2 == 0)
    .Sum();
sw.Stop();
Console.WriteLine($"Параллельно: {sumPar:N0} за {sw.ElapsedMilliseconds}ms");

// Настройка степени параллелизма
var result = numbers
    .AsParallel()
    .WithDegreeOfParallelism(4)  // Максимум 4 потока
    .Where(n => IsPrime(n))
    .Take(100)
    .ToList();

static bool IsPrime(int n)
{
    if (n < 2) return false;
    for (int i = 2; i <= Math.Sqrt(n); i++)
        if (n % i == 0) return false;
    return true;
}

Итого

  • ✅ LINQ — встроенный язык запросов для любых коллекций
  • ✅ Методный синтаксис: .Where().Select().OrderBy()
  • ✅ Query синтаксис: from x in col where ... select ...
  • ✅ Ленивые вычисления — запрос выполняется при переборе
  • ✅ ToList()/ToArray() — материализует запрос немедленно
  • ✅ async/await — асинхронность без блокировки
  • ✅ Task/Task<T> — обёртка для асинхронных операций
  • ✅ PLINQ — параллельная обработка данных

LINQ с XML и JSON

// Пример: работа со списком объектов как с "базой данных"
record Employee(int Id, string Name, string Dept, decimal Salary, int YearsExp);

var employees = new[]
{
    new Employee(1, "Алиса",   "ИТ",         120000m, 5),
    new Employee(2, "Борис",   "Маркетинг",  85000m,  3),
    new Employee(3, "Вера",    "ИТ",         150000m, 8),
    new Employee(4, "Гриша",   "Финансы",    95000m,  4),
    new Employee(5, "Дарья",   "ИТ",         110000m, 4),
    new Employee(6, "Егор",    "Маркетинг",  90000m,  6),
    new Employee(7, "Жанна",   "Финансы",    105000m, 5),
};

// Комплексный анализ
var report = employees
    .GroupBy(e => e.Dept)
    .Select(g => new {
        Dept      = g.Key,
        Count     = g.Count(),
        AvgSalary = g.Average(e => e.Salary),
        TotalCost = g.Sum(e => e.Salary),
        Senior    = g.OrderByDescending(e => e.YearsExp).First().Name,
    })
    .OrderByDescending(d => d.AvgSalary);

Console.WriteLine($"{"Отдел",-15} {"Сотр",5} {"Ср.зарплата",12} {"Общие расходы",15} {"Старший",10}");
Console.WriteLine(new string('-', 65));
foreach (var d in report)
    Console.WriteLine($"{d.Dept,-15} {d.Count,5} {d.AvgSalary,12:C} {d.TotalCost,15:C} {d.Senior,10}");

// Корреляция опыта и зарплаты
var correlation = employees
    .OrderBy(e => e.YearsExp)
    .Select(e => $"{e.YearsExp}г→{e.Salary/1000:F0}К");
Console.WriteLine("\nОпыт → Зарплата: " + string.Join(", ", correlation));