1. Введение
Когда вы смотрите сериал, вы не всегда узнаёте полную информацию обо всех персонажах, только о том, что необходимо для сюжета. В программировании часто возникают похожие ситуации: нам нужен не весь объект целиком, а только отдельные его поля, значения или даже некоторые вычисления на их основе.
Проекция в LINQ — это преобразование элементов исходной последовательности в новую форму. Обычно с помощью метода Select. Можно думать про Select как про волшебную машину, которая берёт каждый элемент, применяет к нему функцию и возвращает результат. Вот и всё, никаких фокусов.
Когда нужна проекция?
- Хотим вывести только имена пользователей, а не все данные о них.
- Готовим email-рассылку: из объекта клиента берём только адрес и имя.
- Делаем расчёт: к цене товара добавляем налог и получаем новую коллекцию.
- Для UI: нужно отобразить только часть данных, а не перегружать интерфейс.
2. Метод Select: синтаксис и базовая работа
Основной синтаксис
LINQ поддерживает два стиля: методический (Method Syntax) и запросный (Query Syntax). Для Select в обоих стилях результат одинаков.
Метод-синтаксис
var query = myCollection.Select(x => x.ЧтоВернуть);
Здесь x — переменная для каждого элемента коллекции, а справа — выражение, превращающее элемент в то, что нужно.
Запрос-синтаксис
var query = from x in myCollection
select x.ЧтоВернуть;
Здесь всё немного больше похоже на SQL.
Простой пример: список чисел
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// Хочу получить их квадраты
var squares = numbers.Select(n => n * n);
foreach (var s in squares)
{
Console.WriteLine(s); // 1, 4, 9, 16, 25
}
Вот только что мы превратили коллекцию чисел в коллекцию их квадратов — мгновенно, не прибегая к ручному циклу!
Пример 2
В прошлых примерах у нас уже был класс Product:
public class Product
{
public string Name { get; set; }
public double Price { get; set; }
}
Допустим, у нас есть список:
var products = new List<Product>
{
new Product { Name = "Шоколад", Price = 120 },
new Product { Name = "Сыр", Price = 250 },
new Product { Name = "Хлеб", Price = 55 }
};
Если мы хотим просто получить имена всех продуктов, нам не нужен весь объект, правда? Нам нужен только список названий:
var names = products.Select(p => p.Name);
foreach (var name in names)
{
Console.WriteLine(name); // Шоколад, Сыр, Хлеб
}
3. Возвращаемая коллекция — какие бывают варианты?
Обратите внимание, что Select всегда возвращает коллекцию (точнее — IEnumerable<TOutput>), где TOutput — тип результата функции. Вы сами определяете, что это будет: строка, число, анонимный тип, даже новый объект.
Проекция в новый тип
Например, вы хотите получить не сырой объект, а его "проекцию":
var projections = products.Select(p => new { p.Name, ЦенаСНДС = p.Price * 1.2 });
foreach (var item in projections)
{
Console.WriteLine($"{item.Name}: {item.ЦенаСНДС}");
}
Здесь мы создали новую коллекцию анонимных объектов — с названием и ценой с учётом воображаемого НДС!
4. Возвращаем новый класс или анонимный тип
Вы можете выбирать: создавать полноценные классы для результата или ограничиться анонимными типами, если структура нужна только "здесь и сейчас".
Анонимные типы
var result = products.Select(p => new { Название = p.Name, СтараяЦена = p.Price, НоваяЦена = p.Price + 40 });
foreach (var p in result)
{
Console.WriteLine($"{p.Название}: Было {p.СтараяЦена}, стало {p.НоваяЦена}");
}
Анонимные типы удобно использовать для быстрого формирования результатов, особенно когда нет смысла плодить классы ради одной операции.
Используем собственный класс для проекции
Создадим новый класс:
public class ProductDto
{
public string Name { get; set; }
public double PriceWithTax { get; set; }
}
Теперь внутри LINQ-запроса:
var productsWithTax = products.Select(p => new ProductDto
{
Name = p.Name,
PriceWithTax = p.Price * 1.2
});
foreach (var p in productsWithTax)
{
Console.WriteLine($"{p.Name}: {p.PriceWithTax}");
}
5. Доступ к внутренним коллекциям и свойствам
Что если внутри объекта есть коллекция? Например, у каждого пользователя есть список заказов.
public class User
{
public string Name { get; set; }
public List<Product> Purchases { get; set; }
}
Вы хотите получить список всех списков покупок? Без проблем!
var allPurchases = users.Select(u => u.Purchases);
foreach (var purchaseList in allPurchases)
{
foreach (var product in purchaseList)
{
Console.WriteLine(product.Name);
}
}
Важно! В этом случае у нас получился не "плоский" список товаров, а коллекция коллекций. Как получить ОДИН список со всеми товарами всех пользователей? Это будет тема следующей лекции — там мы познакомимся с SelectMany.
6. Полезные нюансы
Преобразование к типу коллекции: ToList() и друзья
Select возвращает IEnumerable<T>. Если вы хотите обычный список (List<T>), просто добавьте .ToList():
var nameList = products.Select(p => p.Name).ToList();
Теперь у вас полноценный список, с которым можно работать привычным способом.
Использование индекса элемента
Иногда полезно знать, какой элемент по счёту в коллекции сейчас обрабатывается. У метода Select есть перегрузка:
var indexed = products.Select((product, index) => new { Index = index, Name = product.Name });
foreach (var item in indexed)
{
Console.WriteLine($"{item.Index}: {item.Name}");
}
Так можно, например, подготавливать данные для пронумерованных списков.
Проекция в программировании
Компетентность в Select — частый вопрос на собеседованиях. Без этой конструкции трудно представить современные приложения:
- Выборка только нужных данных из БД или внешних сервисов.
- Подготовка данных для передачи между слоями приложения.
- Преобразование данных в формате, пригодном для отображения на экране или отправки по сети.
- Экономия памяти (не тащим весь объект коллекции, а только нужные поля).
Компании, работающие с большими объёмами данных, зачастую строят на этом основную бизнес-логику.
7. Типичные ошибки и особенности
Иногда начинающие разработчики сталкиваются с раздражающими багами — обычно из-за недопонимания, как работает ленивое выполнение (deferred execution) в LINQ. Например, после вызова Select не всегда происходит немедленное выполнение запроса — только когда вы реально итерируете коллекцию (foreach, ToList(), и т.д.). Это позволяет выстроить "трубопровод обработки", но иногда приводит к тому, что результат меняется, если исходные данные были изменены до момента использования результата запроса.
Также не забывайте, что Select не изменяет исходную коллекцию — он строит новую. Поэтому если вы всё ещё ожидаете найти в products новые поля после products.Select(...), этого не произойдёт.
Обработка null: если проекция предполагает обращение к потенциально null-полям, не забывайте об этом позаботиться. В C# 8+ возникает предупреждение компилятора, если возможно получить NullReferenceException.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ