Когда я пишу код в Visual Studio, ReSharper (да благословит его Бог!) Часто предлагает мне сменить цикл старой школы for на более компактную форму foreach.
И часто, когда я принимаю это изменение, ReSharper делает шаг вперед и предлагает мне изменить его снова, в блестящей форме LINQ.
Итак, мне интересно: есть ли реальные преимущества в этих улучшениях? В довольно простом выполнении кода я не вижу никакого увеличения скорости (очевидно), но я вижу, что код становится все менее и менее читабельным ... Итак, я задаюсь вопросом: оно того стоит?
foreach
- удаление элементов из коллекции при ее перечислении, где обычно требуетсяfor
цикл для запуска с последнего элемента.Ответы:
for
противforeach
Существует распространенное заблуждение, что эти две конструкции очень похожи и что они взаимозаменяемы, как это:
а также:
Тот факт, что оба ключевых слова начинаются с одинаковых трех букв, не означает, что семантически они похожи. Эта путаница чрезвычайно подвержена ошибкам, особенно для начинающих. Перебор коллекции и выполнение чего-либо с элементами сделано с помощью
foreach
;for
не должен и не должен использоваться для этой цели , если вы действительно не знаете, что делаете.Давайте посмотрим, что не так с этим на примере. В конце вы найдете полный код демонстрационного приложения, используемого для сбора результатов.
В этом примере мы загружаем некоторые данные из базы данных, точнее, города из Adventure Works, упорядоченные по имени, прежде чем встретить «Бостон». Используется следующий запрос SQL:
Данные загружаются
ListCities()
методом, который возвращаетIEnumerable<string>
. Вот как этоforeach
выглядит:Давайте перепишем его
for
, предполагая, что оба они взаимозаменяемы:Оба возвращают одни и те же города, но есть огромная разница.
foreach
,ListCities()
вызывается один раз и дает 47 пунктов.for
,ListCities()
называется 94 раз и дает 28153 пунктов в целом.Что случилось?
IEnumerable
это ленивый . Это означает, что он будет выполнять работу только в тот момент, когда нужен результат. Ленивая оценка - это очень полезная концепция, но она имеет некоторые предостережения, в том числе тот факт, что легко упустить момент (ы), когда потребуется результат, особенно в тех случаях, когда результат используется несколько раз.В случае a
foreach
результат запрашивается только один раз. В случае,for
как это реализовано в неправильно написанном коде выше , результат запрашивается 94 раза , то есть 47 × 2:Каждый раз
cities.Count()
называется (47 раз),Каждый раз
cities.ElementAt(i)
называется (47 раз).Запрашивать базу данных 94 раза вместо одной ужасно, но не хуже, что может случиться. Представьте, например, что произойдет, если
select
запросу предшествует запрос, который также вставляет строку в таблицу. Правильно, у нас было бы то,for
что будет вызывать базу данных 2 147 483 647 раз, если мы не надеемся, что она завершится раньше.Конечно, мой код необъективен. Я сознательно воспользовался своей ленью
IEnumerable
и написал это так, чтобы неоднократно звонитьListCities()
. Можно заметить, что новичок никогда этого не сделает, потому что:Свойство
IEnumerable<T>
не имеетCount
, а только методCount()
. Вызов метода является пугающим, и можно ожидать, что его результат не будет кэширован и непригоден дляfor (; ...; )
блока.Индексирование недоступно для
IEnumerable<T>
и не очевидно, чтобы найтиElementAt
метод расширения LINQ.Вероятно, большинство новичков просто преобразуют результат
ListCities()
в нечто, с чем они знакомы, напримерList<T>
.Тем не менее, этот код сильно отличается от
foreach
альтернативного. Опять же, он дает те же результаты, и на этот разListCities()
метод вызывается только один раз, но дает 575 элементов, в то время как с помощьюforeach
он дает только 47 элементов.Разница исходит из того , что
ToList()
вызывает все данные должны быть загружены из базы данных. В то время какforeach
запрошены только города до «Бостона», новыйfor
требует, чтобы все города были извлечены и сохранены в памяти. С 575 короткими строками это, вероятно, не имеет большого значения, но что если мы извлекаем только несколько строк из таблицы, содержащей миллиарды записей?Так что же на
foreach
самом деле?foreach
ближе к циклу времени. Код, который я ранее использовал:можно просто заменить на:
Оба производят один и тот же IL. Оба имеют одинаковый результат. Оба имеют одинаковые побочные эффекты. Конечно, это
while
можно переписать аналогичным образомfor
, но оно будет еще длиннее и подвержено ошибкам. Вы можете выбрать тот, который вы найдете более читабельным.Хотите сами это проверить? Вот полный код:
И результаты:
LINQ против традиционного способа
Что касается LINQ, вы можете изучить функциональное программирование (FP) - не материал C # FP, а настоящий язык FP, такой как Haskell. Функциональные языки имеют особый способ выражения и представления кода. В некоторых ситуациях он превосходит нефункциональные парадигмы.
Известно, что FP намного лучше, когда дело доходит до манипулирования списками ( список как общий термин, не связанный с
List<T>
). Учитывая этот факт, возможность выражать код C # более функциональным способом, когда дело доходит до списков, является довольно хорошей вещью.Если вы не уверены, сравните читабельность кода, написанного как функциональными, так и нефункциональными способами, в моем предыдущем ответе на эту тему.
источник
foreach
это более эффективно, чемfor
, когда на самом деле несоответствие является результатом намеренно нарушенного кода. Тщательность ответа оправдывает себя, но легко увидеть, как случайный наблюдатель может прийти к неправильным выводам.Хотя уже есть несколько отличных экспозиций о различиях между for и foreach. Есть некоторые грубые искажения роли LINQ.
Синтаксис LINQ - это не просто синтаксический сахар, дающий приближение функционального программирования к C #. LINQ предоставляет функциональные конструкции, включая все их преимущества для C #. В сочетании с возвратом IEnumerable вместо IList, LINQ обеспечивает отложенное выполнение итерации. То, что люди обычно делают сейчас, это создать и вернуть IList из своих функций, например
Вместо этого используйте синтаксис yield return для создания отложенного перечисления.
Теперь перечисление не произойдет, пока вы не внесете ToList или не выполните итерацию по нему. И это происходит только по мере необходимости (вот перечисление Fibbonaci, у которого нет проблемы переполнения стека)
Выполнение foreach над функцией Фибоначчи вернет последовательность 46. Если вы хотите 30-е, это все, что будет вычислено
Там, где мы получаем массу удовольствия, это поддержка языка лямбда-выражений (в сочетании с конструкциями IQueryable и IQueryProvider это позволяет функционально составлять запросы к различным наборам данных, IQueryProvider отвечает за интерпретацию переданного в выражения и создание и выполнение запроса с использованием собственных конструкций источника). Я не буду вдаваться в подробности здесь, но есть серия постов в блоге, показывающих, как создать поставщик SQL-запросов здесь
Таким образом, вы должны предпочесть возвращать IEnumerable вместо IList, когда потребители вашей функции выполнят простую итерацию. И использовать возможности LINQ для отсрочки выполнения сложных запросов до тех пор, пока они не потребуются.
источник
Читаемость в глазах смотрящего. Некоторые люди могут сказать
отлично читается; другие могут сказать, что это непрозрачно, и предпочли бы
как прояснение того, что делается. Мы не можем сказать вам, что вы находите более читабельным. Но вы, возможно, сможете обнаружить некоторые из моих собственных предвзятых мнений в примере, который я построил здесь ...
источник
Разница между LINQ и
foreach
действительно сводится к двум различным стилям программирования: императивному и декларативному.Императив: в этом стиле вы говорите компьютеру: «сделай это ... теперь сделай это ... теперь сделай это, теперь сделай это». Вы кормите его программой по одному шагу за раз.
Декларативный: в этом стиле вы сообщаете компьютеру, каким должен быть результат, и позволяете ему понять, как этого добиться.
Классическим примером этих двух стилей является сравнение кода сборки (или C) с SQL. В сборке вы даете инструкции (буквально) по одному. В SQL вы выражаете, как объединить данные и какой результат вы хотите получить из этих данных.
Приятным побочным эффектом декларативного программирования является то, что он имеет тенденцию быть немного более высокого уровня. Это позволяет платформе развиваться под вами без необходимости изменять ваш код. Например:
Что здесь происходит? Distinct использует одно ядро? Два? Пятьдесят? Мы не знаем и нам все равно. Разработчики .NET могут переписать его в любое время, если он продолжает выполнять ту же самую задачу, наш код мог бы просто волшебным образом ускориться после обновления кода.
Это сила функционального программирования. И причина, по которой вы найдете этот код в таких языках, как Clojure, F # и C # (написанный с использованием мышления функционального программирования), часто в 3–10 раз меньше, чем у его обязательных аналогов.
Наконец, мне нравится декларативный стиль, потому что в большинстве случаев это позволяет мне писать код, который не изменяет данные. В приведенном выше примере,
Distinct()
не меняет бар, он возвращает новую копию данных. Это означает, что какой бы ни был бар, и откуда бы он ни пришел, он не внезапно изменился.Итак, как говорят другие авторы, изучите функциональное программирование. Это изменит вашу жизнь. И если вы можете, сделайте это на настоящем функциональном языке программирования. Я предпочитаю Clojure, но F # и Haskell также являются отличным выбором.
источник
var foo = bar.Distinct()
по существу,IEnumerator<T>
пока вы не позвоните.ToList()
или.ToArray()
. Это важное различие, потому что, если вы не знаете об этом, это может привести к трудностям для понимания ошибок.Могут ли другие разработчики в команде читать LINQ?
Если нет, не используйте его, или произойдет одно из двух:
A для каждого цикла идеально подходит для итерации по списку, но если это не то, что вам нужно делать, не используйте его.
источник