за vs. foreach і LINQ


86

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

І часто, коли я приймаю цю зміну, ReSharper робить крок вперед і пропонує мені змінити її знову, у блискучій формі LINQ.

Отже, мені цікаво: чи є якісь реальні переваги в цих удосконаленнях? У досить простому виконанні коду я не бачу збільшення швидкості (очевидно), але я можу бачити, що код стає все менш читабельним ... Тож мені цікаво: чи варто це?


2
Лише зауважте - синтаксис LINQ насправді досить читабельний, якщо ви знайомі з синтаксисом SQL. Існує також два формати для LINQ (лямбда-вирази, подібні SQL, і ланцюгові методи), які можуть полегшити навчання. Просто пропозиції ReSharper можуть зробити його нечитабельним.
Шауна

3
Як правило, я зазвичай використовую функцію foreach, крім випадків, коли працюю з відомим масивом довжини або подібними випадками, коли кількість ітерацій є релевантним. Що стосується LINQ - якщо я його використовую, я зазвичай бачу, що ReSharper робить передбачуваним, і якщо отримане LINQ твердження є акуратним / тривіальним / читабельним, я використовую його, інакше повертаю його назад. Якщо було б клопотанням переписати оригінальну логіку, яка не є LINQ, якщо вимоги змінені, або якщо може знадобитися детально відладжувати логіку через логіку, виписка LINQ абстрагується, я не LINQ і залишаю її довго форма.
Ед Гастінгс

однією поширеною помилкою foreachє видалення елементів із колекції під час її перерахування, де зазвичай потрібен forцикл для запуску з останнього елемента.
Слай

Ви можете взяти цінність від Øredev 2013 - Jessica Kerr - Функціональні принципи для об'єктно-орієнтованих розробників . Linq виходить на презентацію незабаром після 33-хвилинної позначки під заголовком "Декларативний стиль".
Тераот

Відповіді:


139

for vs. 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;
        }
    }
}

Обидва виробляють однаковий ІР. Обидва мають однаковий результат. Обидва мають однакові побічні ефекти. Звичайно, це 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 позицій.

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

Дані називались 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) дозволяє запит передаватись і складати до тих пір, поки він не буде ітераційним (означає, що додавання пункту де до поверненого IQueryable призведе до того, що SQL передається серверу при ітерації, включаючи те, де пункт вивантаження фільтра на сервер).
Майкл Браун

8
Наскільки мені подобається ця відповідь, я думаю, що приклади дещо надумані. Підсумок в кінці підказує, що foreachце більш ефективно, ніж for, коли фактично невідповідність є результатом навмисно порушеного коду. Ретельність відповіді викуповує себе, але легко зрозуміти, як випадковий спостерігач може прийти до неправильних висновків.
Роберт Харві

19

Незважаючи на те, що вже існують чудові експозиції щодо відмінностей між «і». Існують деякі грубі хибні уявлення про роль LINQ.

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

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

Замість цього використовуйте синтаксис повернення доходу для створення відкладеного перерахунку.

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;
}

Тепер перерахування не відбуватиметься, поки ви не зробите це списком або повторите його. І це відбувається лише у міру необхідності (ось перелік 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;
  }
}

Виконуючи передбачення над функцією Фібоначчі, повернете послідовність 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;
        }
    }
}

як зрозуміліше, що робиться. Ми не можемо сказати тобі, що вважаєш читабельнішим. Але ви, можливо, зможете виявити деякі мої власні ухили на прикладі, який я побудував тут ...


28
Чесно скажу, що Linq робить намір об'єктивно більш читабельним, тоді як для циклів роблять механізм об'єктивнішим для читання.
jk.

16
Я біг би якомога швидше від того, хто скаже мені, що версія for-for-if є більш читаною, ніж пересічна версія.
Конаміман

3
@Konamiman - Це залежало б від того, що людина шукає, коли думає про "читабельність". Коментар jk. це чудово ілюструє це. Цикл є більш читабельним у тому сенсі, що ви можете легко побачити, як він отримує свій кінцевий результат, тоді як LINQ читає, яким повинен бути кінцевий результат.
Шауна

2
Ось чому цикл переходить у реалізацію, і тоді ви використовуєте Intersect скрізь.
Р. Мартіньо Фернандес

8
@Shauna: уявіть версію for-loop всередині методу, який виконує кілька інших речей; це безлад. Таким чином, ви, природно, розділите це на власний метод. Зчитування з розумом - це те саме, що IEnumerable <T> .Intersect, але тепер ви дублювали функціональність фреймворку та ввели більше коду для підтримки. Єдине виправдання - якщо вам потрібна спеціальна реалізація з поведінкових причин, але тут ми говоримо лише про читабельність.
Місько

7

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

  • Імператив: у цьому стилі ви кажете комп’ютеру "зробіть це ... зараз зробіть це ... тепер це зробіть зараз". Ви подаєте програму покроково.

  • Декларативний: у цьому стилі ви повідомляєте комп’ютеру, яким ви хочете бути результатом, і даєте йому зрозуміти, як дістатися.

Класичним прикладом цих двох стилів є порівняння коду складання (або C) з SQL. На зборах ви даєте інструкції (буквально) по черзі. У SQL ви виражаєте, як об’єднати дані разом та який результат потрібно отримати від цих даних.

Приємним побічним ефектом декларативного програмування є те, що він, як правило, трохи вищий рівень. Це дозволяє платформі розвиватися під вами, не змінюючи код. Наприклад:

var foo = bar.Distinct();

Що тут відбувається? Чи розрізнення використовується одне ядро? Два? П’ятдесят? Ми не знаємо і нам все одно. Розробники .NET могли переписати його в будь-який час, доки він продовжує виконувати ту саму мету, що наш код міг просто чарівно швидше отримати після оновлення коду.

Це сила функціонального програмування. І причина в тому, що ви знайдете цей код на таких мовах, як Clojure, F # і C # (написаний за допомогою функціонального мислення для програмування), часто на 3x-10x менший, ніж його обов'язкові аналоги.

Нарешті, мені подобається стиль декларації, тому що в C # більшість часу це дозволяє мені писати код, який не мутує дані. У наведеному вище прикладі Distinct()не змінюється панель, вона повертає нову копію даних. Це означає, що який би бар не був, і де б він ніколи не з’явився, він не раптом змінився.

Так, як говорять інші афіші, вивчіть функціональне програмування. Це змінить ваше життя. А якщо можете, зробіть це справжньою функціональною мовою програмування. Я віддаю перевагу Clojure, але F # і Haskell - це також відмінний вибір.


2
Обробка LINQ відкладається, поки ви фактично не повторите її. var foo = bar.Distinct()є , по суті, IEnumerator<T>поки ви не викличете .ToList()або .ToArray(). Це важлива відмінність, оскільки якщо ви не знаєте про це, це може призвести до важких для розуміння помилок.
Берін Лорич

-5

Чи можуть інші розробники в команді читати LINQ?

Якщо ні, то не використовуйте його, або станеться одна з двох речей:

  1. Ваш код буде неможливим
  2. Ви будете застрягли у збереженні всього свого коду та всього, що на нього покладається

Кожен цикл ідеально підходить для повторення списку, але якщо це не те, що робити, то не використовуйте його.


11
хм, я розумію, що для одного проекту це може бути відповіддю, але в середньостроковій та довгостроковій перспективі ви повинні навчити своїх співробітників, інакше у вас є гонка до глибини розуміння коду, що не здається гарною ідеєю.
jk.

21
Насправді, може трапитися і третє: інші розробники могли докласти малих зусиль і насправді дізнатися щось нове та корисне. Це не нечувано.
Ерік Кінг

6
@InvertedLlama, якби я був у компанії, де розробникам потрібна формальна підготовка для розуміння нових мовних понять, то я б думав над тим, щоб знайти нову компанію.
Wyatt Barnett

13
Можливо, ви можете піти з таким ставленням до бібліотек, але якщо мова йде про основні мовні особливості, це не скорочує. Ви можете вибирати рамки. Але хороший .NET-програміст повинен розуміти кожну особливість мови та основної платформи (System. *). І враховуючи, що ви навіть не можете правильно використовувати EF, не використовуючи Linq, я мушу сказати ... в цей день і вік, якщо ви програміст .NET і не знаєте Linq, ви некомпетентні.
Тімоті Болдрідж

7
Для цього вже достатньо зворотних запитів, тому я до цього не додаду, але аргумент, що підтримує неосвічених / некомпетентних колег, ніколи не є дійсним.
Стівен Еверс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.