Course program← C#: Базовый курс
1Знакомство с C# и .NET
2Данные и типы
3Ветвления и циклы
4Методы и ошибки
5Коллекции и строки
6Классы и объекты

Знакомство с LINQ

This lesson is not translated yet – the Russian original is below.

Половина работы с коллекциями – это отобрать нужное, преобразовать и посчитать. Писать под каждую такую задачу цикл утомительно, поэтому в 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;

Это ровно тот же запрос, что и цепочка выше, – компилятор переводит одно в другое. Форма с точками встречается чаще, потому что в неё легко добавить операции, у которых нет ключевого слова. Запросную форму стоит уметь читать, но писать можно любую.