LINQ: Получить топ-10 популярных книг
Условие
В системе управления библиотекой есть следующие классы:
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.
Задание:
- Написать LINQ-запрос для получения топ-10 книг по количеству выдач
- Написать запрос для получения всех уникальных категорий (используя SelectMany)
- Написать запрос для группировки книг по автору с подсчётом общего количества выдач
- Объяснить разницу между методами First, FirstOrDefault, Single и SingleOrDefault
Критерии оценки:
- Правильность LINQ-синтаксиса
- Понимание отложенного выполнения
- Эффективность запросов
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Анализ задачи
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();
Что происходит:
OrderByDescending()сортирует коллекцию в порядке убыванияTake(10)берёт первые 10 элементов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:
- ✅ Отложенное выполнение экономит ресурсы
- ✅
SelectManyдля развёртывания вложенных коллекций - ✅
GroupByдля агрегирования и анализа - ✅ Правильно выбирайте
FirstvsSingle— это предохранитель от ошибок - ✅ Материализуйте результат только когда нужно
.ToList() - ✅ В Entity Framework используйте
Include()для избежания N+1