Половина работы с коллекциями – это отобрать нужное, преобразовать и посчитать. Писать под каждую такую задачу цикл утомительно, поэтому в C# есть набор готовых операций. Называется он LINQ.
Цикл против запроса
Задача: получить чётные числа из списка. Обычным циклом это выглядит так:
var evens = new List<int>();
foreach (var number in numbers)
{
if (number % 2 == 0)
{
evens.Add(number);
}
}
Восемь строк, из которых смысл несут полторы. То же самое запросом:
var evens = numbers.Where(n => n % 2 == 0).ToList();
Читается как предложение: «взять из numbers те, где остаток от деления на два равен нулю».
Для LINQ нужен using System.Linq; – в новых проектах он подключён автоматически.
Лямбда-выражение
Запись n => n % 2 == 0 – это лямбда, безымянный метод, записанный на месте:
- слева от стрелки – параметр (имя выбираете сами);
- справа – выражение, которое возвращается.
Тип параметра не пишут: он известен из коллекции. Если numbers – это List<int>, то n – это
int.
Лямбду можно читать вслух: n => n > 5 – «для n вернуть, больше ли n пяти».
Имя n ничем не лучше x или number. В коротких лямбдах принято брать первую букву смысла: u => u.Age для пользователя, o => o.Total для заказа.
Основные операции
Where – отобрать подходящие:
var adults = users.Where(u => u.Age >= 18);
Select – преобразовать каждый элемент:
var names = users.Select(u => u.Name); // из пользователей – только имена
var squares = numbers.Select(n => n * n);
OrderBy – упорядочить:
var sorted = users.OrderBy(u => u.Age);
var reverse = users.OrderByDescending(u => u.Age);
Подсчёты – одним вызовом:
int count = numbers.Count(n => n > 5);
int total = numbers.Sum();
double average = numbers.Average();
int max = numbers.Max();
Поиск одного элемента:
var first = users.First(u => u.Age > 18); // упадёт, если нет
var maybe = users.FirstOrDefault(u => u.Age > 18); // null, если нет
bool exists = users.Any(u => u.Age > 18); // просто есть или нет
bool allAdults = users.All(u => u.Age >= 18); // все ли подходят
Пара First и FirstOrDefault различается поведением при пустом результате: первый бросает исключение, второй возвращает значение по умолчанию. Выбирайте по смыслу: если отсутствие элемента – это ошибка в данных, берите First, он сообщит о ней сразу.
Цепочки
Операции соединяются, и порядок имеет значение:
var result = users
.Where(u => u.Age >= 18)
.OrderBy(u => u.Name)
.Select(u => u.Name)
.ToList();
Читается сверху вниз: отобрали взрослых, упорядочили по имени, взяли только имена, собрали в список.
Фильтровать выгоднее раньше: Where перед OrderBy оставляет сортировке меньше работы.
Отложенное выполнение
Тонкость, из-за которой LINQ иногда ведёт себя неожиданно:
var evens = numbers.Where(n => n % 2 == 0); // пока ничего не произошло
numbers.Add(10);
foreach (var n in evens) // вот сейчас выполняется фильтрация
{
Console.WriteLine(n); // десятка попадёт в результат
}
Where возвращает не готовый список, а описание запроса. Работа начинается в момент перебора –
поэтому изменения коллекции, сделанные позже, попадают в результат.
Чтобы зафиксировать результат сейчас, его материализуют:
var evens = numbers.Where(n => n % 2 == 0).ToList();
Правило простое: если результат перебирается один раз – оставляйте как есть. Если он нужен несколько раз или коллекция может измениться – вызывайте ToList(). Без этого запрос будет выполняться заново при каждом переборе.
LINQ ничего не меняет
Все операции возвращают новую последовательность, а исходную коллекцию оставляют в покое:
var sorted = numbers.OrderBy(n => n);
Console.WriteLine(numbers[0]); // порядок в numbers прежний
Сравните с Array.Sort, который сортирует на месте. Разница принципиальная, и путать их не стоит:
LINQ описывает, что вы хотите получить, а не что нужно сделать с данными.
Вторая форма записи
У LINQ есть синтаксис, похожий на SQL:
var result = from u in users
where u.Age >= 18
orderby u.Name
select u.Name;
Это ровно тот же запрос, что и цепочка выше, – компилятор переводит одно в другое. Форма с точками встречается чаще, потому что в неё легко добавить операции, у которых нет ключевого слова. Запросную форму стоит уметь читать, но писать можно любую.