← Назад к вопросам

LINQ: Получить топ-10 популярных книг

2.2 Middle🔥 191 комментариев
#Основы C# и .NET

Условие

В системе управления библиотекой есть следующие классы:

class Book { public int Id { get; set; } public string Title { get; set; } public string Author { get; set; } public int BorrowCount { get; set; } public List<string> Categories { get; set; } }

Дан список книг List<Book> books.

Задание:

  1. Написать LINQ-запрос для получения топ-10 книг по количеству выдач
  2. Написать запрос для получения всех уникальных категорий (используя SelectMany)
  3. Написать запрос для группировки книг по автору с подсчётом общего количества выдач
  4. Объяснить разницу между методами First, FirstOrDefault, Single и SingleOrDefault

Критерии оценки:

  • Правильность LINQ-синтаксиса
  • Понимание отложенного выполнения
  • Эффективность запросов

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Анализ задачи

LINQ (Language Integrated Query) — это мощный инструмент для работы с коллекциями данных. Ключевые моменты:
  • Отложенное выполнение — запросы выполняются только при обращении к результатам
  • Два синтаксиса — методы расширения и query expressions
  • Деferred execution — результаты вычисляются лениво

1. Топ-10 книг по количеству выдач

Синтаксис методов (Method Syntax):

var top10Books = books
    .OrderByDescending(b => b.BorrowCount)  // Сортировка по выдачам (убывающий порядок)
    .Take(10)                                  // Берём первые 10
    .ToList();                                 // Материализуем результат

Query Expression Syntax:

var top10Books = (
    from b in books
    orderby b.BorrowCount descending
    select b
)
.Take(10)
.ToList();

Что происходит:

  1. OrderByDescending() сортирует коллекцию в порядке убывания
  2. Take(10) берёт первые 10 элементов
  3. ToList() материализует результат — выполняет запрос и преобразует в список

Важно: Без ToList() запрос не выполняется (отложенное выполнение).


2. Все уникальные категории (SelectMany)

Проблема: Каждая книга имеет List<string> категорий. Нужно получить все категории в одном плоском списке.

Неправильный подход:

var categories = books.Select(b => b.Categories);  // Даст List<List<string>>

Правильный подход — SelectMany (Flatten):

// SelectMany развёртывает вложенные коллекции в одну
var allCategories = books
    .SelectMany(b => b.Categories)  // Развёртываем все списки категорий в один
    .Distinct()                        // Берём только уникальные
    .OrderBy(c => c)                   // Сортируем (опционально)
    .ToList();

Как это работает:

Книги:
- "C# Guide" → Categories: ["Programming", "CSharp"]
- "Java Book" → Categories: ["Programming", "Java"]

SelectMany даст: ["Programming", "CSharp", "Programming", "Java"]
Distinct даст:   ["Programming", "CSharp", "Java"]

Query Expression Syntax:

var allCategories = (
    from book in books
    from category in book.Categories
    select category
)
.Distinct()
.ToList();

SelectMany vs Select:

// ❌ Select — даст List<List<string>>
books.Select(b => b.Categories).Count();  // 5 (5 книг)

// ✅ SelectMany — даст List<string>
books.SelectMany(b => b.Categories).Count();  // 15 (если всего 15 категорий)

3. Группировка книг по автору с суммой выдач

var booksByAuthor = books
    .GroupBy(b => b.Author)  // Группируем по автору
    .Select(group => new
    {
        Author = group.Key,  // Ключ группы (имя автора)
        TotalBorrows = group.Sum(b => b.BorrowCount),  // Сумма выдач в группе
        BookCount = group.Count(),  // Количество книг
        Books = group.Select(b => b.Title).ToList()  // Названия всех книг автора
    })
    .OrderByDescending(a => a.TotalBorrows)  // Сортируем по популярности
    .ToList();

Результат (примерный):

// {
//   Author = "Bjarne Stroustrup",
//   TotalBorrows = 5420,
//   BookCount = 3,
//   Books = ["The C++ Programming Language", "A Tour of C++", ...]
// },
// {
//   Author = "Eric Evans",
//   TotalBorrows = 3210,
//   BookCount = 2,
//   Books = ["Domain-Driven Design", ...]
// }

Query Expression Syntax:

var booksByAuthor = (
    from book in books
    group book by book.Author into authorGroup
    select new
    {
        Author = authorGroup.Key,
        TotalBorrows = authorGroup.Sum(b => b.BorrowCount),
        BookCount = authorGroup.Count()
    }
)
.OrderByDescending(a => a.TotalBorrows)
.ToList();

4. Разница между First, FirstOrDefault, Single и SingleOrDefault

МетодВозвращаетИсключение если пусто?Исключение если >1?Когда использовать
First()Первый элементДа (InvalidOperationException)НетУверены, что есть элемент
FirstOrDefault()Первый элемент или defaultНетНетНе уверены, есть ли элемент
Single()Единственный элементДаДа (если >1)Ожидаете ровно 1 результат
SingleOrDefault()Единственный или defaultНетДа (если >1)Ожидаете 0 или 1 результат

Примеры:

var books = new List<Book> { ... };

// ✅ First() — если уверены, что список не пуст
var firstBook = books.First();

// ❌ First() на пустом списке — выкидывает исключение
var emptyBooks = books.Where(b => b.Id > 9999).First();  // InvalidOperationException!

// ✅ FirstOrDefault() — безопаснее
var firstOrNull = books.FirstOrDefault();  // Вернёт null если пусто

// ✅ Single() — когда ищем уникальный элемент (например, по ID)
var bookById = books.Single(b => b.Id == 42);  // Исключение если не найдено

// ❌ Single() выкидывает если >1 результата
var cSharpBooks = books.Single(b => b.Title.Contains("C#"));  // Может быть несколько!

// ✅ SingleOrDefault() — безопаснее для неуникальных запросов
var singleBook = books.SingleOrDefault(b => b.Id == 42);

Практический пример:

// Получение книги по ID (ожидаем ровно 1)
var book = books.SingleOrDefault(b => b.Id == searchId);
if (book == null)
    Console.WriteLine("Книга не найдена");

// Получение первой книги какой-то категории (может быть много)
var firstProgramming = books
    .Where(b => b.Categories.Contains("Programming"))
    .FirstOrDefault();

// Опасно! Может выбросить исключение если много результатов
// var singleProgramming = books.Single(b => b.Categories.Contains("Programming"));

Отложенное выполнение (Deferred Execution)

Это ключевая особенность LINQ!

// ❌ Запрос НЕ выполняется сейчас, создаётся IEnumerable
var query = books
    .Where(b => b.BorrowCount > 100)
    .Select(b => b.Title);

Console.WriteLine(query.Count());  // Только ВОТ ЗДЕСЬ выполняется

// ✅ Чтобы выполнить сразу, материализуем результат
var result = books
    .Where(b => b.BorrowCount > 100)
    .Select(b => b.Title)
    .ToList();  // Выполняется ВОТ ЗДЕСЬ

Материализующие методы (Force Evaluation):

  • .ToList() → List<T>
  • .ToArray() → T[]
  • .ToDictionary() → Dictionary<K, V>
  • .First(), .Single(), .Count() → Scalar value
  • .Any(), .All() → bool

Оптимизация LINQ-запросов

❌ Неэффективно — materializes все, потом берёт 10:

var result = books
    .OrderByDescending(b => b.BorrowCount)
    .ToList()  // Здесь сортируется весь список!
    .Take(10)  // Только потом берём 10
    .ToList();

✅ Эффективно — Take применяется к отложенному выполнению:

var result = books
    .OrderByDescending(b => b.BorrowCount)
    .Take(10)  // Ленивое выполнение
    .ToList();  // Материализуем только 10 элементов

❌ N+1 проблема (если книги связаны с другой таблицей):

// Запрос 1 для всех книг, потом 1 запрос для каждой категории
var books = context.Books.ToList();
var categorized = books.Select(b => new { b.Title, b.Categories }).ToList();

✅ Eager Loading:

// Один запрос с JOIN
var categorized = context.Books
    .Include(b => b.Categories)  // В Entity Framework
    .ToList();

Полный практический пример

public class BookAnalytics
{
    public static void AnalyzeBooks(List<Book> books)
    {
        // 1. Топ-10 популярных
        var top10 = books
            .OrderByDescending(b => b.BorrowCount)
            .Take(10)
            .ToList();

        // 2. Все категории
        var categories = books
            .SelectMany(b => b.Categories)
            .Distinct()
            .ToList();

        // 3. Статистика по авторам
        var authorStats = books
            .GroupBy(b => b.Author)
            .Select(g => new AuthorStatistics
            {
                Author = g.Key,
                TotalBorrows = g.Sum(b => b.BorrowCount),
                AverageBorrows = g.Average(b => b.BorrowCount),
                BookCount = g.Count()
            })
            .OrderByDescending(a => a.TotalBorrows)
            .ToList();

        // 4. Книги по категориям
        var booksByCategory = books
            .SelectMany(b => b.Categories, (b, c) => new { Book = b, Category = c })
            .GroupBy(bc => bc.Category)
            .Select(g => new
            {
                Category = g.Key,
                Books = g.Select(bc => bc.Book.Title).ToList(),
                Count = g.Count()
            })
            .ToList();
    }
}

public class AuthorStatistics
{
    public string Author { get; set; }
    public int TotalBorrows { get; set; }
    public double AverageBorrows { get; set; }
    public int BookCount { get; set; }
}

Выводы

Основные принципы LINQ:

  1. ✅ Отложенное выполнение экономит ресурсы
  2. SelectMany для развёртывания вложенных коллекций
  3. GroupBy для агрегирования и анализа
  4. ✅ Правильно выбирайте First vs Single — это предохранитель от ошибок
  5. ✅ Материализуйте результат только когда нужно .ToList()
  6. ✅ В Entity Framework используйте Include() для избежания N+1