1. Введение
Фильтрация — это выборка из коллекции только тех элементов, которые отвечают определённому условию. Представьте: в вашей базе — 10 000 товаров, а менеджеру нужно «только» 12, которые подходят под новый хитрый критерий. Писать каждый раз вручную перебор и кучу проверок — удовольствие сомнительное, а код становится нечитаемым. LINQ берёт эти хлопоты на себя.
В реальных приложениях фильтрация — самая частая операция с данными: показываем пользователю только те записи, которые ему интересны, «чистим» коллекцию от нежелательных элементов, делаем выборки для отчётов, поиска или отправки e-mail. На собеседованиях вопросы о LINQ-фильтрации встречаются постоянно. Понимание этой темы — залог успеха для начинающего .NET-разработчика.
2. Знакомимся с методом Where
Смысл и принцип работы
Метод Where — это расширяющий метод LINQ, который принимает функцию (или лямбда-выражение), определяющую условие фильтрации. Он возвращает новую последовательность (не изменяя исходную!), в которой содержатся только те элементы, для которых условие возвращает true.
Сигнатура основная:
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate)
Не пугайтесь generics и страшных слов. Сейчас важно понять общую идею: Where принимает исходную коллекцию и условие, которое вы задаете. На практике всё просто:
- source — та коллекция, которую фильтруем (например, List<Product>)
- predicate — функция/условие (например, p => p.Price > 100)
Простая задача для затравки
Допустим, у нас есть список товаров, и мы хотим выбрать только те, у которых цена больше 100. В старые-добрые времена писали бы цикл foreach и вручную добавляли нужные элементы в новый список. LINQ позволяет сделать это одной строкой.
3. Фильтрация
Наш класс Product и стартовая коллекция
В прошлом модуле мы писали примерно такой класс товаров:
class Product
{
public string Name { get; set; }
public double Price { get; set; }
// Для красоты вывода на экран:
public override string ToString()
{
return $"{Name} (цена: {Price})";
}
}
Допустим, у нас есть список товаров:
var products = new List<Product>
{
new Product { Name = "Хлеб", Price = 30 },
new Product { Name = "Молоко", Price = 87 },
new Product { Name = "Сыр", Price = 250 },
new Product { Name = "Шоколад", Price = 130 }
};
Фильтрация товаров дороже 100 с помощью Where
var expensiveProducts = products.Where(p => p.Price > 100);
foreach (var product in expensiveProducts)
{
Console.WriteLine(product);
}
Что происходит под капотом:
- Where перебирает все элементы списка products,
- Для каждого вызывает функцию (в нашем случае: p => p.Price > 100),
- Если функция вернула true, товар попадает в новую коллекцию.
Результат на экране:
Сыр (цена: 250)
Шоколад (цена: 130)
Заметьте: исходный список products не изменился! LINQ не разрушает ваши данные.
4. Ленивая фильтрация (Deferred Execution)
Интересный и важный момент: результат работы Where вычисляется, только когда вы реально обращаетесь к элементам. Например, как только начинается цикл foreach, LINQ «идёт» по исходной коллекции и фильтрует элементы налету. Если вы не используете результат, никакой работы не происходит. Это называется отложенное выполнение (deferred execution).
Это даёт нам кучу плюсов:
- Можно строить очень длинные цепочки методов (например, и отфильтровать, и отсортировать), и только при первом реальном запросе к результату всё выполняется.
- Экономия памяти: не происходит промежуточных лишних преобразований коллекций.
- При необходимости можно отменить выполнение или «прервать» после нужного количества найденных элементов.
Визуальная схема
Исходная коллекция —► Where (условие) —► Перебираем только нужные элементы
[ х ] [ х ] [ + ] [ + ] —► p.Price > 100 —► [ + ] [ + ]
(х — элемент не удовлетворяет условию, + — удовлетворяет)
5. Синтаксические варианты
Классический метод расширения (Method Syntax)
Этот стиль мы уже используем:
var result = products.Where(p => p.Price > 100);
Альтернатива: Query Syntax
LINQ поддерживает синтаксис, похожий на SQL-запросы:
var result = from p in products
where p.Price > 100
select p;
Результат — тот же!
Выбор между синтаксисом — дело вкуса, но method syntax (точка-методы) встречается чаще, особенно в реальных проектах и при построении длинных цепочек.
6. Сложные условия фильтрации
Несколько условий
Вы можете использовать логические операторы (&&, ||, !):
// Найти все дорогие товары, кроме "Шоколад"
var filtered = products.Where(
p => p.Price > 100 && p.Name != "Шоколад"
);
Фильтрация по строке (подстрока, регистр, Contains)
// Товары, в названии которых есть буква "л"
var lProducts = products.Where(p => p.Name.Contains("л"));
Обратите внимание: Contains чувствителен к регистру! Если нужен «без учета регистра», можно так:
var lProducts = products.Where(p => p.Name
.ToLower().Contains("л")); // но теперь "Молоко" и "Шоколад" оба попадут в выборку
Фильтрация по нескольким коллекциям
Допустим, у вас есть два списка — товары и платежи. Можно фильтровать товары, которые встречаются в списке платежей — но для этого лучше использовать методы Any и Join, о которых будем говорить дальше. Просто знайте: LINQ умеет фильтровать и более сложными способами!
7. Полезные нюансы
Вложенная фильтрация и цепочки методов
Вы можете объединять несколько этапов фильтрации, вызывая Where несколько раз подряд:
var filtered = products
.Where(p => p.Price > 100)
.Where(p => p.Name.StartsWith("Ш"));
Однако лучше объединять условия в один Where с помощью логических операторов — так эффективнее и код проще.
Встроенные компараторы и кастомные функции
Иногда стандартных операторов мало. Например, нужно фильтровать строки без учета регистра и с учетом культуры. В этом случае удобно использовать методы сравнения:
var rusProducts = products.Where(
p => p.Name.StartsWith("ш", StringComparison.OrdinalIgnoreCase)
);
8. Фильтрация и пользовательский ввод
Попробуем сделать простую командную фильтрацию на практике! Например, спросим у пользователя минимальную цену, потом покажем все подходящие товары.
Console.Write("Минимальная цена товара? ");
var minPriceStr = Console.ReadLine();
if (double.TryParse(minPriceStr, out double minPrice))
{
var filtered = products.Where(p => p.Price >= minPrice);
foreach (var product in filtered)
{
Console.WriteLine(product);
}
}
else
{
Console.WriteLine("Ошибка: введено не число.");
}
Теперь наше "мини-приложение" уже почти выглядит как магазин!
9. Типичные ошибки и грабли при использовании Where
В программировании, как и в жизни, встречаются подводные камни. Вот чего стоит опасаться.
Забыли вызвать ToList() или ToArray()
Результат работы Where — это не список и даже не массив! Это объект IEnumerable<Product>. Чаще всего он прекрасно работает в цикле foreach, но если нужна именно коллекция (например, если планируете индексированный доступ), не забудьте вызвать .ToList() или .ToArray():
var filteredList = products.Where(p => p.Price > 100).ToList();
Если игнорировать этот момент, легко получить ошибку вроде "Collection was modified during enumeration" — особенно если вы решите изменить исходную коллекцию во время перебора.
Изменение исходной коллекции
Поскольку LINQ-фильтрация отложенная, если в середине перебора вы удаляете элементы из исходного списка, может возникнуть исключение. Особенно это касается многопоточных сценариев или случаев, когда коллекция меняется прямо в процессе фильтрации.
Null-значения и предикаты
Если коллекция содержит null, а вы в предикате обращаетесь к полю объекта без проверки, получите NullReferenceException. Например:
var filtered = products.Where(p => p.Name.StartsWith("А"));
Если какой-то элемент в products равен null, код сломается.
Рекомендация: всегда добавляйте проверку на null, если есть риск наличия таких элементов:
var filtered = products.Where(p => p != null && p.Name.StartsWith("А"));
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ