за против foreach против LINQ

86

Когда я пишу код в Visual Studio, ReSharper (да благословит его Бог!) Часто предлагает мне сменить цикл старой школы for на более компактную форму foreach.

И часто, когда я принимаю это изменение, ReSharper делает шаг вперед и предлагает мне изменить его снова, в блестящей форме LINQ.

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

beccoblu
источник
2
Просто заметьте - синтаксис LINQ на самом деле довольно читабелен, если вы знакомы с синтаксисом SQL. Существует также два формата для LINQ (лямбда-выражения в стиле SQL и цепочечные методы), которые могут облегчить изучение. Это могут быть только предложения ReSharper, которые делают его нечитаемым.
Шона
3
Как правило, я обычно использую foreach, если не работаю с массивом известной длины или аналогичными случаями, когда количество итераций является релевантным. Что касается LINQ-ifying, я обычно посмотрю, что ReSharper делает из foreach, и если получающийся оператор LINQ будет аккуратным / тривиальным / читабельным, я использую его, а в противном случае я возвращаю его обратно. Если было бы непросто переписать исходную логику, не относящуюся к LINQ, если требования изменились, или если может потребоваться детальная отладка с помощью логики, от которой абстрагируется оператор LINQ, я не LINQ и оставляю ее надолго форма.
Эд Гастингс
Одна распространенная ошибка foreach- удаление элементов из коллекции при ее перечислении, где обычно требуется forцикл для запуска с последнего элемента.
Слай
Вы могли бы оценить ценность Øredev 2013 - Джессика Керр - Функциональные принципы для объектно-ориентированных разработчиков . Linq появляется на презентации вскоре после 33-минутной отметки под названием «Декларативный стиль».
Theraot

Ответы:

139

for против foreach

Существует распространенное заблуждение, что эти две конструкции очень похожи и что они взаимозаменяемы, как это:

foreach (var c in collection)
{
    DoSomething(c);
}

а также:

for (var i = 0; i < collection.Count; i++)
{
    DoSomething(collection[i]);
}

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

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

В этом примере мы загружаем некоторые данные из базы данных, точнее, города из Adventure Works, упорядоченные по имени, прежде чем встретить «Бостон». Используется следующий запрос SQL:

select distinct [City] from [Person].[Address] order by [City]

Данные загружаются ListCities()методом, который возвращает IEnumerable<string>. Вот как это foreachвыглядит:

foreach (var city in Program.ListCities())
{
    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

Давайте перепишем его for, предполагая, что оба они взаимозаменяемы:

var cities = Program.ListCities();
for (var i = 0; i < cities.Count(); i++)
{
    var city = cities.ElementAt(i);

    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

Оба возвращают одни и те же города, но есть огромная разница.

  • При использовании 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>.

var cities = Program.ListCities();
var flushedCities = cities.ToList();
for (var i = 0; i < flushedCities.Count; i++)
{
    var city = flushedCities[i];

    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

Тем не менее, этот код сильно отличается от foreachальтернативного. Опять же, он дает те же результаты, и на этот раз ListCities()метод вызывается только один раз, но дает 575 элементов, в то время как с помощью foreachон дает только 47 элементов.

Разница исходит из того , что ToList()вызывает все данные должны быть загружены из базы данных. В то время как foreachзапрошены только города до «Бостона», новый forтребует, чтобы все города были извлечены и сохранены в памяти. С 575 короткими строками это, вероятно, не имеет большого значения, но что если мы извлекаем только несколько строк из таблицы, содержащей миллиарды записей?

Так что же на foreachсамом деле?

foreachближе к циклу времени. Код, который я ранее использовал:

foreach (var city in Program.ListCities())
{
    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

можно просто заменить на:

using (var enumerator = Program.ListCities().GetEnumerator())
{
    while (enumerator.MoveNext())
    {
        var city = enumerator.Current;
        Console.Write(city + " ");

        if (city == "Boston")
        {
            break;
        }
    }
}

Оба производят один и тот же IL. Оба имеют одинаковый результат. Оба имеют одинаковые побочные эффекты. Конечно, это whileможно переписать аналогичным образом for, но оно будет еще длиннее и подвержено ошибкам. Вы можете выбрать тот, который вы найдете более читабельным.

Хотите сами это проверить? Вот полный код:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;

public class Program
{
    private static int countCalls;

    private static int countYieldReturns;

    public static void Main()
    {
        Program.DisplayStatistics("for", Program.UseFor);
        Program.DisplayStatistics("for with list", Program.UseForWithList);
        Program.DisplayStatistics("while", Program.UseWhile);
        Program.DisplayStatistics("foreach", Program.UseForEach);

        Console.WriteLine("Press any key to continue...");
        Console.ReadKey(true);
    }

    private static void DisplayStatistics(string name, Action action)
    {
        Console.WriteLine("--- " + name + " ---");

        Program.countCalls = 0;
        Program.countYieldReturns = 0;

        var measureTime = Stopwatch.StartNew();
        action();
        measureTime.Stop();

        Console.WriteLine();
        Console.WriteLine();
        Console.WriteLine("The data was called {0} time(s) and yielded {1} item(s) in {2} ms.", Program.countCalls, Program.countYieldReturns, measureTime.ElapsedMilliseconds);
        Console.WriteLine();
    }

    private static void UseFor()
    {
        var cities = Program.ListCities();
        for (var i = 0; i < cities.Count(); i++)
        {
            var city = cities.ElementAt(i);

            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseForWithList()
    {
        var cities = Program.ListCities();
        var flushedCities = cities.ToList();
        for (var i = 0; i < flushedCities.Count; i++)
        {
            var city = flushedCities[i];

            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseForEach()
    {
        foreach (var city in Program.ListCities())
        {
            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseWhile()
    {
        using (var enumerator = Program.ListCities().GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                var city = enumerator.Current;
                Console.Write(city + " ");

                if (city == "Boston")
                {
                    break;
                }
            }
        }
    }

    private static IEnumerable<string> ListCities()
    {
        Program.countCalls++;
        using (var connection = new SqlConnection("Data Source=mframe;Initial Catalog=AdventureWorks;Integrated Security=True"))
        {
            connection.Open();

            using (var command = new SqlCommand("select distinct [City] from [Person].[Address] order by [City]", connection))
            {
                using (var reader = command.ExecuteReader(CommandBehavior.SingleResult))
                {
                    while (reader.Read())
                    {
                        Program.countYieldReturns++;
                        yield return reader["City"].ToString();
                    }
                }
            }
        }
    }
}

И результаты:

--- для ---
Абингдон Олбани Александрия Альгамбра [...] Бонн Бордо Бостон

Данные были названы 94 раз (а) и дали 28153 пункта (ов).

--- со списком ---
Абингдон Олбани Александрия Альгамбра [...] Бонн Бордо Бостон

Данные были вызваны 1 раз (а) и дали 575 пунктов.

--- в то время ---
Абингдон Олбани Александрия Альгамбра [...] Бонн Бордо Бостон

Данные были вызваны 1 раз (а) и дали 47 пунктов.

--- foreach ---
Абингдон Олбани Александрия Альгамбра [...] Бонн Бордо Бостон

Данные были вызваны 1 раз (а) и дали 47 пунктов.

LINQ против традиционного способа

Что касается LINQ, вы можете изучить функциональное программирование (FP) - не материал C # FP, а настоящий язык FP, такой как Haskell. Функциональные языки имеют особый способ выражения и представления кода. В некоторых ситуациях он превосходит нефункциональные парадигмы.

Известно, что FP намного лучше, когда дело доходит до манипулирования списками ( список как общий термин, не связанный с List<T>). Учитывая этот факт, возможность выражать код C # более функциональным способом, когда дело доходит до списков, является довольно хорошей вещью.

Если вы не уверены, сравните читабельность кода, написанного как функциональными, так и нефункциональными способами, в моем предыдущем ответе на эту тему.

Арсений Мурзенко
источник
1
Вопрос о примере ListCities (). Почему он запускается только один раз? У меня не было проблем с прогнозированием доходности в прошлом.
Данте
1
Он не говорит, что вы получите только один результат из IEnumerable - он говорит, что SQL-запрос (который является дорогой частью метода) будет выполняться только один раз - это хорошо. Затем он прочитает и выдаст все результаты запроса.
HappyCat
9
@ Джорджио: Хотя этот вопрос понятен, наличие семантики языкового потворства тому, что новичка может показаться запутанным, не оставит нас с очень эффективным языком.
Стивен Эверс
4
LINQ не просто семантический сахар. Это обеспечивает отложенное выполнение. И в случае IQueryables (например, Entity Framework) позволяет передавать и составлять запрос до тех пор, пока он не будет повторен (это означает, что добавление предложения where к возвращенному IQueryable приведет к тому, что SQL, переданный серверу после итерации, будет включать предложение where разгрузка фильтрации на сервер).
Майкл Браун
8
Как бы мне ни нравился этот ответ, я думаю, что примеры несколько надуманы. Резюме в конце предполагает, что foreachэто более эффективно, чем for, когда на самом деле несоответствие является результатом намеренно нарушенного кода. Тщательность ответа оправдывает себя, но легко увидеть, как случайный наблюдатель может прийти к неправильным выводам.
Роберт Харви
19

Хотя уже есть несколько отличных экспозиций о различиях между for и foreach. Есть некоторые грубые искажения роли LINQ.

Синтаксис LINQ - это не просто синтаксический сахар, дающий приближение функционального программирования к C #. LINQ предоставляет функциональные конструкции, включая все их преимущества для C #. В сочетании с возвратом IEnumerable вместо IList, LINQ обеспечивает отложенное выполнение итерации. То, что люди обычно делают сейчас, это создать и вернуть IList из своих функций, например

public IList<Foo> GetListOfFoo()
{
   var retVal=new List<Foo>();
   foreach(var foo in _myPrivateFooList)
   {
      if(foo.DistinguishingValue == check)
      {
         retVal.Add(foo);
      }
   }
   return retVal;
}

Вместо этого используйте синтаксис yield return для создания отложенного перечисления.

public IEnumerable<Foo> GetEnumerationOfFoo()
{
   //no need to create an extra list
   //var retVal=new List<Foo>();
   foreach(var foo in _myPrivateFooList)
   {
      if(foo.DistinguishingValue == check)
      {
         //yield the match compiler handles the complexity
         yield return foo;
      }
   }
   //no need for returning a list
   //return retVal;
}

Теперь перечисление не произойдет, пока вы не внесете ToList или не выполните итерацию по нему. И это происходит только по мере необходимости (вот перечисление Fibbonaci, у которого нет проблемы переполнения стека)

/**
Returns an IEnumerable of fibonacci sequence
**/
public IEnumerable<int> Fibonacci()
{
  int first, second = 1;
  yield return first;
  yield return second;
  //the 46th fibonacci number is the largest that
  //can be represented in 32 bits. 
  for (int i = 3; i < 47; i++)
  {
    int retVal = first+second;
    first=second;
    second=retVal;
    yield return retVal;
  }
}

Выполнение foreach над функцией Фибоначчи вернет последовательность 46. Если вы хотите 30-е, это все, что будет вычислено

var thirtiethFib=Fibonacci().Skip(29).Take(1);

Там, где мы получаем массу удовольствия, это поддержка языка лямбда-выражений (в сочетании с конструкциями IQueryable и IQueryProvider это позволяет функционально составлять запросы к различным наборам данных, IQueryProvider отвечает за интерпретацию переданного в выражения и создание и выполнение запроса с использованием собственных конструкций источника). Я не буду вдаваться в подробности здесь, но есть серия постов в блоге, показывающих, как создать поставщик SQL-запросов здесь

Таким образом, вы должны предпочесть возвращать IEnumerable вместо IList, когда потребители вашей функции выполнят простую итерацию. И использовать возможности LINQ для отсрочки выполнения сложных запросов до тех пор, пока они не потребуются.

Майкл Браун
источник
13

но я вижу, что код становится все менее и менее читабельным

Читаемость в глазах смотрящего. Некоторые люди могут сказать

var common = list1.Intersect(list2);

отлично читается; другие могут сказать, что это непрозрачно, и предпочли бы

List<int> common = new List<int>();
for(int i1 = 0; i1 < list1.Count; i1++)
{
    for(int i2 = 0; i2 < list2.Count; i2++)
    {
        if (list1[i1] == list2[i2])
        {
            common.Add(i1);
            break;
        }
    }
}

как прояснение того, что делается. Мы не можем сказать вам, что вы находите более читабельным. Но вы, возможно, сможете обнаружить некоторые из моих собственных предвзятых мнений в примере, который я построил здесь ...

AakashM
источник
28
Честно говоря, я бы сказал, что Linq объективно делает намерение более читабельным, а циклы for делают механизм объективно более читабельным.
JK.
16
Я бы побежал так быстро, как только мог, от кого-то, кто сказал бы мне, что версия «for-for-if» более читаема, чем версия с пересечением.
Konamiman
3
@Konamiman - Это будет зависеть от того, что человек ищет, когда он думает о «читабельности». Комментарий JK. прекрасно иллюстрирует это. Цикл является более читабельным в том смысле, что вы можете легко увидеть, как он получает конечный результат, в то время как LINQ более читабелен, каким должен быть конечный результат.
Шона
2
Вот почему цикл входит в реализацию, и тогда вы везде используете Intersect.
Р. Мартиньо Фернандес
8
@Shauna: вообразите версию цикла внутри метода, делающую несколько других вещей; это беспорядок. Поэтому, естественно, вы разбили его на собственный метод. С точки зрения читаемости, это то же самое, что и IEnumerable <T> .Intersect, но теперь вы продублировали функциональность фреймворка и ввели больше кода для поддержки. Единственное оправдание - если вам нужна пользовательская реализация по поведенческим причинам, но мы говорим только о читабельности здесь.
Миско
7

Разница между LINQ и foreachдействительно сводится к двум различным стилям программирования: императивному и декларативному.

  • Императив: в этом стиле вы говорите компьютеру: «сделай это ... теперь сделай это ... теперь сделай это, теперь сделай это». Вы кормите его программой по одному шагу за раз.

  • Декларативный: в этом стиле вы сообщаете компьютеру, каким должен быть результат, и позволяете ему понять, как этого добиться.

Классическим примером этих двух стилей является сравнение кода сборки (или C) с SQL. В сборке вы даете инструкции (буквально) по одному. В SQL вы выражаете, как объединить данные и какой результат вы хотите получить из этих данных.

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

var foo = bar.Distinct();

Что здесь происходит? Distinct использует одно ядро? Два? Пятьдесят? Мы не знаем и нам все равно. Разработчики .NET могут переписать его в любое время, если он продолжает выполнять ту же самую задачу, наш код мог бы просто волшебным образом ускориться после обновления кода.

Это сила функционального программирования. И причина, по которой вы найдете этот код в таких языках, как Clojure, F # и C # (написанный с использованием мышления функционального программирования), часто в 3–10 раз меньше, чем у его обязательных аналогов.

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

Итак, как говорят другие авторы, изучите функциональное программирование. Это изменит вашу жизнь. И если вы можете, сделайте это на настоящем функциональном языке программирования. Я предпочитаю Clojure, но F # и Haskell также являются отличным выбором.

Тимоти Болдридж
источник
2
Обработка LINQ откладывается до тех пор, пока вы фактически не выполните итерацию. var foo = bar.Distinct()по существу, IEnumerator<T>пока вы не позвоните .ToList()или .ToArray(). Это важное различие, потому что, если вы не знаете об этом, это может привести к трудностям для понимания ошибок.
Берин Лорич
-5

Могут ли другие разработчики в команде читать LINQ?

Если нет, не используйте его, или произойдет одно из двух:

  1. Ваш код будет недоступен
  2. Вы застрянете с поддержанием всего своего кода и всего, что зависит от него

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

Перевернутая лама
источник
11
хм, я ценю, что для отдельного проекта это может быть ответом, но в среднесрочной и долгосрочной перспективе вы должны обучать свой персонал, в противном случае вы столкнетесь с проблемой понимания кода, что не очень хорошая идея.
JK.
21
На самом деле, может случиться и третье: другие разработчики могут приложить немного усилий и научиться чему-то новому и полезному. Это не неслыханно.
Эрик Кинг,
6
@ InvertedLlama, если бы я был в компании, где разработчикам нужно формальное обучение для понимания новых языковых концепций, я бы подумал о поиске новой компании.
Уайетт Барнетт
13
Возможно, вам удастся избежать такого отношения к библиотекам, но когда речь идет о базовых языковых возможностях, это не мешает. Вы можете выбрать рамки. Но хороший программист .NET должен понимать все функции языка и базовой платформы (System. *). И учитывая, что вы не можете даже использовать EF должным образом без использования Linq, я должен сказать ... в наши дни, если вы программист .NET и не знаете Linq, вы некомпетентны.
Тимоти Болдридж
7
У этого уже есть достаточно отрицательных голосов, поэтому я не буду добавлять к этому, но аргумент, поддерживающий невежественных / некомпетентных коллег, никогда не будет действительным.
Стивен Эверс