Аналитический отдел хочет отчёт по активным игрокам: только те, кто заходил в последние 30 дней, имеет уровень выше 10 и не забанен. Раньше для каждого нового фильтра приходилось добавлять ещё один if внутри цикла.
Задача: Из списка игроков выберите тех, кто соответствует всем трём условиям одновременно. Попробуйте сделать это с помощью Where.
В прошлых уроках мы увидели Where мельком — в составе цепочки. Сегодня разберём его по-настоящему. Это самый используемый метод LINQ — с него начинается почти любой запрос. Если LINQ — это язык запросов, то Where — его главное слово.
Давайте начнём с простого. У нас есть список игроков, и мы хотим оставить только магов. Без LINQ мы бы написали так:
var mages = new List<Player>();
foreach (var p in players)
{
if (p.Class == "Маг")
mages.Add(p);
}
// 4 строки ради одной мысли: "дай мне только магов"
С Where та же мысль записывается в одну строку:
var mages = players.Where(p => p.Class == "Маг");
Вот и всё. Одна строка вместо четырёх. Но давайте разберём, что тут происходит, потому что в этой строке спрятано несколько важных вещей.
Параметр p => p.Class == "Маг" — это лямбда-выражение. Оно принимает игрока и возвращает true или false. Такую функцию-условие называют предикатом.
Представьте себе охранника на входе в VIP-зону. Он проверяет каждого гостя по списку. Если гость в списке — проходит. Если нет — разворачивается. Предикат — это правило, по которому охранник принимает решение.
Where— это сам охранник. Теперь давайте усложним. Аналитический отдел хочет отчёт по активным игрокам: только тех, кто заходил в последние 30 дней, имеет уровень выше 10 и не забанен. Три условия одновременно.
Первый инстинкт — три вложенных if. Но в LINQ условия просто комбинируются через && и || прямо внутри лямбды:
var activePlayers = players.Where(p =>
p.LastLogin >= DateTime.Now.AddDays(-30)
&& p.Level > 10
&& !p.IsBanned);
Три условия — одна строка фильтрации. Читается как обычное предложение: «где последний вход свежее 30 дней, и уровень больше 10, и не забанен».
А можно ли записать это по-другому? Да — через цепочку нескольких Where:
var activePlayers = players
.Where(p => p.LastLogin >= DateTime.Now.AddDays(-30))
.Where(p => p.Level > 10)
.Where(p => !p.IsBanned);
Результат тот же самый. Производительность тоже одинаковая — LINQ оптимизирует это под капотом.
А вот кое-что, о чём многие не знают. У Where есть перегрузка, которая передаёт в лямбду не только элемент, но и его индекс в коллекции:
// Выбрать элементы на чётных позициях (0, 2, 4...)
var everySecond = players.Where((p, index) => index % 2 == 0);
Это бывает полезно для специфических задач — например, взять каждый третий элемент из лога или выбрать элементы по их позиции. Не самая частая потребность, но когда она возникает — приятно знать, что Where это умеет.
Where работает не только со списками объектов. Он работает с любой коллекцией. Вот пример со строкой — выберем только цифры из текста:
string input = "Альберт: уровень 50, золото 1200";
var digits = input.Where(c => char.IsDigit(c));
string numbers = new string(digits.ToArray());
Console.WriteLine(numbers); // "501200"
Строка в C# — это IEnumerable<char>, а значит, все LINQ-методы к ней применимы. Это может показаться неожиданным, но это работает и бывает очень удобно.
А вот пример со словарём — отберём предметы дороже 100 золотых:
var inventory = new Dictionary<string, int>
{
{ "Зелье здоровья", 50 },
{ "Меч огня", 500 },
{ "Щит льда", 300 },
{ "Свиток телепорта", 80 }
};
var expensive = inventory.Where(item => item.Value > 100);
foreach (var item in expensive)
Console.WriteLine($"{item.Key}: {item.Value} золотых");
// Меч огня: 500 золотых
// Щит льда: 300 золотых
Обратите внимание: при работе со словарём Where перебирает пары KeyValuePair<string, int>. Поэтому в лямбде мы обращаемся к item.Key и item.Value.
И ещё один момент, который стоит запомнить: Where — это фильтрация, не трансформация. Он не меняет элементы — только решает, какие оставить, а какие выбросить. Если нужно изменить форму элементов — для этого есть Select, о котором поговорим в следующем уроке.
Where — метод фильтрации. Принимает предикат (функцию, возвращающую bool) и оставляет только элементы, для которых предикат вернул true.
Условия комбинируются через && и || в одном Where, или через цепочку нескольких Where — результат одинаковый.
Перегрузка Where((element, index) => ...) даёт доступ к индексу элемента.
Where не изменяет исходную коллекцию.