LINQ

Если вы потратили много времени на C # и .NET, вероятно, вы столкнулись с LINQ (Language-Integrated Query), который позволяет вам использовать ряд мощных возможностей запросов непосредственно на языке C #.

В приведенном ниже примере демонстрируется пара общих функций LINQ (обратите внимание, что я использую синтаксис метода расширения, а не выражения LINQ). В этом примере у нас есть список людей, и мы хотим получить список имен взрослых в этом списке. Затем мы дважды переберем эти имена (это будет полезно для демонстрации различий между немедленным и отложенным выполнением).

Используя LINQ, мы можем:

  • Отфильтруйте по возрасту с помощью Where
  • Сопоставьте объект Person со строкой имени с помощью Select
  • Преобразовать запрос в список с помощью ToList

[pastacode lang=»c» manual=»public%20class%20Person%0A%7B%0A%20%20%20%20public%20string%20Name%20%7B%20get%3B%20set%3B%20%7D%0A%20%20%20%20public%20int%20Age%20%7B%20get%3B%20set%3B%20%7D%0A%7D%0A%0Avar%20people%20%3D%20new%20List%3CPerson%3E%0A%7B%0A%20%20%20%20new%20Person%20%7B%20Name%20%3D%20%22Sam%22%2C%20Age%20%3D%2027%20%7D%2C%0A%20%20%20%20new%20Person%20%7B%20Name%20%3D%20%22Suzie%22%2C%20Age%20%3D%2017%20%7D%2C%0A%20%20%20%20new%20Person%20%7B%20Name%20%3D%20%22Harry%22%2C%20Age%20%3D%2023%20%7D%2C%0A%7D%3B%0A%0Avar%20adultNames%20%3D%20people%0A%20%20%20%20.Where(person%20%3D%3E%20%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20Console.WriteLine(%22Filtering%20by%20age…%22)%3B%0A%20%20%20%20%20%20%20%20return%20person.Age%20%3E%3D%2018%3B%0A%20%20%20%20%7D)%0A%20%20%20%20.Select(person%20%3D%3E%20person.Name)%0A%20%20%20%20.ToList()%3B%0A%0Aforeach(var%20name%20in%20adultNames)%0A%20%20%20%20Console.Writeline(name)%3B%0A%0Aforeach(var%20name%20in%20adultNames)%0A%20%20%20%20Console.Writeline(name)%3B%0A%0A%2F*%20output%0AFiltering%20by%20age%0AFiltering%20by%20age%0AFiltering%20by%20age%0ASam%0AHarry%0ASam%0AHarry%0A*%2F%0A» message=»» highlight=»» provider=»manual»/]

Обратите внимание, что в приведенном выше примере мы явно конвертируем запрос в список. Это немедленно выполняет запрос, чтобы дать новый список, содержащий только имена взрослых, которые мы затем можем перебирать.

Итак, что произойдет, если мы оставим ToList?

[pastacode lang=»c» manual=»var%20adultNames%20%3D%20people%0A%20%20%20%20.Where(person%20%3D%3E%20%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20Console.WriteLine(%22Filtering%20by%20age…%22)%3B%0A%20%20%20%20%20%20%20%20return%20person.Age%20%3E%3D%2018%3B%0A%20%20%20%20%7D)%0A%20%20%20%20.Select(person%20%3D%3E%20person.Name)%3B%0A%0Aforeach(var%20name%20in%20adultNames)%0A%20%20%20%20Console.Writeline(name)%3B%0A%0Aforeach(var%20name%20in%20adultNames)%0A%20%20%20%20Console.Writeline(name)%3B%0A%0A%2F*%20output%0AFiltering%20by%20age%0ASam%0AFiltering%20by%20age%0AFiltering%20by%20age%0AHarry%0AFiltering%20by%20age%0ASam%0AFiltering%20by%20age%0AFiltering%20by%20age%0AHarry%0A*%2F%0A» message=»» highlight=»» provider=»manual»/]

Теперь результат выглядит совсем иначе. Вместо того, чтобы сначала выполнять всю фильтрацию, а затем перебирать имена взрослых, фильтрация для каждого элемента теперь происходит непосредственно перед тем, как мы оцениваем этот элемент. Важно отметить, что фильтрация также происходит каждый раз, когда мы перебираем элементы. Это называется отложенным выполнением, поскольку мы ждем, пока нам действительно не понадобятся значения для оценки запроса.

Преимущества отложенного исполнения

Похоже, что отложенное выполнение является поведением LINQ по умолчанию, если только вы явно не укажете ему немедленно выполнить оценку (используя ToList, ToDictionary и т. д.). Так что в этом должна быть какая-то польза, верно?

1. Лучшая производительность

В большинстве случаев ожидается, что отложенное выполнение приведет к повышению производительности, поскольку вам не нужно выполнять запрос сразу для всего набора данных. Вместо этого вы выполняете запрос по одному элементу за раз, поскольку вы уже выполняете итерацию по нему.

2. Построение запроса

Поскольку нет необходимости выполнять запрос немедленно, вы можете создать его в несколько этапов, возможно, пропустив дополнительную условную логику. Это дает вам дополнительные возможности для создания более сложных запросов.

[pastacode lang=»c» manual=»public%20IEnumerable%3CPerson%3E%20GetNames(IEnumerable%3CPerson%3E%20people%2C%20bool%20onlyAdults)%0A%7B%0A%20%20%20%20var%20query%20%3D%20people.AsEnumerable()%3B%0A%0A%20%20%20%20if%20(onlyAdults)%0A%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%2F%2F%20only%20add%20this%20filter%20when%20onlyAdults%20is%20true%0A%20%20%20%20%20%20%20%20query%20%3D%20query.Where(person%20%3D%3E%20person.Age%20%3E%3D%2018)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20query%20%3D%20query.Select(person%20%3D%3E%20person.Name)%3B%0A%0A%20%20%20%20return%20query.ToList()%3B%0A%7D» message=»» highlight=»» provider=»manual»/]

3. Всегда переоценен

Поскольку запрос всегда переоценивается при каждом перечислении, вы можете добавлять / удалять / изменять элементы своей коллекции после того, как запрос был построен, и запрос будет знать об этих изменениях. Таким образом, вы знаете, что всегда просматриваете самые свежие данные.

[pastacode lang=»c» manual=»var%20people%20%3D%20new%20List%3CPerson%3E%0A%7B%0A%20%20%20%20new%20Person%20%7B%20Name%20%3D%20%22Sam%22%2C%20Age%20%3D%2027%20%7D%2C%0A%20%20%20%20new%20Person%20%7B%20Name%20%3D%20%22Suzie%22%2C%20Age%20%3D%2017%20%7D%2C%0A%20%20%20%20new%20Person%20%7B%20Name%20%3D%20%22Harry%22%2C%20Age%20%3D%2023%20%7D%2C%0A%7D%3B%0A%0Avar%20adultNames%20%3D%20people%0A%20%20%20%20.Where(person%20%3D%3E%20person.Age%20%3E%3D%2018)%0A%20%20%20%20.Select(person%20%3D%3E%20person.Name)%3B%0A%0Aforeach(var%20name%20in%20adultNames)%0A%20%20%20%20Console.WriteLine(name)%3B%0A%0Apeople.Add(new%20Person%20%7B%20Name%20%3D%20%22Sally%22%2C%20Age%20%3D%2026%20%7D)%3B%0A%0Aforeach(var%20name%20in%20adultNames)%0A%20%20%20%20Console.WriteLine(name)%3B%0A%0A%2F*%20output%0ASam%0AHarry%0ASam%0AHarry%0ASally%0A*%2F%0A» message=»» highlight=»» provider=»manual»/]

Подводные камни отсрочки исполнения

Несмотря на то, что до сих пор восхвалялось отложенное выполнение LINQ, этот пост был вдохновлен некоторыми проблемами, с которыми я столкнулся при его использовании. Одно из его преимуществ также является ловушкой, если вы недостаточно внимательны при написании кода — запрос всегда переоценивается.

Хотя в целом отложенное выполнение рассматривается как повышение производительности, могут быть случаи, когда оно действительно может значительно замедлить работу вашего приложения, если вы не будете осторожны. Каждый раз, когда вы знаете, что вам нужно будет повторить итерацию по одной и той же коллекции несколько раз (например, вложенный цикл for / foreach), убедитесь, что вы сначала получаете список. В противном случае вы будете оценивать всю коллекцию каждый раз, что резко снизит производительность. Это особенно верно, если исходная коллекция велика, поскольку, даже если ваш запрос выполняет большую фильтрацию, запрос будет применяться каждый раз ко всей исходной коллекции.

Последняя ошибка, которую следует упомянуть, — это использование Select для запуска набора задач. Я видел аргументы, говорящие о том, что это то, чего вам вообще не следует делать, но я видел это достаточно в кодовых базах, чтобы знать, что люди делают именно это и о чем вам следует знать. Представьте себе сценарий ниже:

[pastacode lang=»c» manual=»var%20listOfIds%20%3D%20new%20List%3Cint%3E%20%7B%201%2C%205%2C%208%2C%2015%20%7D%3B%0A%0Avar%20tasks%20%3D%20listOfIds.Select(id%20%3D%3E%20_repository.GetAsync(id))%3B%0Aawait%20Task.WhenAll(tasks)%3B%0Avar%20results%20%3D%20tasks.Select(task%20%3D%3E%20task.Result).ToList()%3B» message=»» highlight=»» provider=»manual»/]

В приведенном выше примере метод GetAsync фактически выполняется дважды для каждого идентификатора: один раз при первом объявлении и второй раз при выгрузке запроса с помощью ToList. Это не только оказывает огромное влияние на производительность за счет многократного выполнения дорогостоящих операций, но также, поскольку задача повторно выполняется, ее выполнение не гарантируется, когда вы действительно приходите для ее оценки. Как вы можете себе представить, это также особенно опасно, если задача, которую вы выполняете, на самом деле является операцией создания или обновления (да, я тоже это видел). Для того чтобы обезопасить себя, необходимо сразу выгрузить запрос в список:

[pastacode lang=»c» manual=»var%20tasks%20%3D%20listOfIds.Select(id%20%3D%3E%20_repository.GetAsync(id)).ToList()%3B» message=»» highlight=»» provider=»manual»/]

Вывод

В этой статье я представил отложенное выполнение в контексте LINQ. Я показал некоторые его особенности и почему он может быть выгоднее по сравнению с немедленным выполнением. Наконец, я обсудил некоторые распространенные ошибки, на которые следует обратить внимание при использовании LINQ и отложенном выполнении.