foreach vs someList.ForEach () {}


167

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

Перший тип:

List<string> someList = <some way to init>
foreach(string s in someList) {
   <process the string>
}

Інший шлях:

List<string> someList = <some way to init>
someList.ForEach(delegate(string s) {
    <process the string>
});

Я вважаю, що в самому верху голови, що замість анонімного делегата, який я використовую вище, у вас буде делегат для багаторазового використання, який ви можете вказати ...


Відповіді:


134

Існує одна важлива і корисна відмінність між ними.

Оскільки .ForEach використовує forцикл для ітерації колекції, це справедливо (редагувати: до .net 4.5 - реалізація змінилася, і вони обидва кидають):

someList.ForEach(x => { if(x.RemoveMe) someList.Remove(x); }); 

тоді як foreachвикористовує нумератор, тому це невірно:

foreach(var item in someList)
  if(item.RemoveMe) someList.Remove(item);

tl; dr: НЕ копіюйте цей код у свою програму!

Ці приклади не найкраща практика, вони лише для демонстрації відмінностей між ForEach()та foreach.

Видалення елементів зі списку в forциклі може мати побічні ефекти. Найпоширеніший з них описаний у коментарях до цього питання.

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


35
навіть тоді вам слід скористатись someList.RemoveAll (x => x.RemoveMe) замість цього
Марк Cidade

2
З Linq все можна зробити краще. Я щойно показував приклад модифікації колекції в рамках foreach ...

2
RemoveAll () - метод у списку <T>.
Марк Сідаде

3
Ви, напевно, це знаєте, але люди повинні остерігатися видалення предметів таким чином; якщо ви вилучите елемент N, то ітерація пропустить елемент (N + 1), і ви не побачите його у своєму делеґаті або отримаєте шанс його видалити так само, як якщо б ви це зробили для власного циклу.
El Zorko

9
Якщо ви повторите список назад за допомогою циклу for, ви можете видалити елементи без проблем зі зміщенням індексу.
S. Tarık Çetin

78

Ми мали деякий код тут (в VS2005 і C # 2.0) , де попередні інженери вийшли зі свого шляху , щоб використовувати list.ForEach( delegate(item) { foo;});замість foreach(item in list) {foo; };для всього коду , що вони написали. наприклад, блок коду для читання рядків з DataReader.

Я досі не знаю точно, чому вони зробили це.

Недоліками list.ForEach()є:

  • Це більш багатослівно в C # 2.0. Однак у C # 3 далі ви можете використовувати =>синтаксис " ", щоб зробити деякі добре стислі вирази.

  • Він менш знайомий. Люди, яким доводиться підтримувати цей код, будуть задаватися питанням, чому ви зробили це саме так. Мені знадобилося деякий час, щоб вирішити, що немає ніяких причин, окрім можливо зробити так, щоб автор здавався розумним (якість решти коду це підірвала). Це також було менш читабельним, " })" в кінці блоку коду делегата.

  • Дивіться також книгу Білла Вагнера "Ефективний C #: 50 конкретних способів поліпшити свій C #", де він розповідає про те, чому foreach віддають перевагу іншим циклам, як для циклів "або" - головне, що ви дозволяєте компілятору вирішити найкращий спосіб побудови. петля. Якщо в майбутній версії компілятора вдасться скористатися більш швидкою технікою, ви отримаєте це безкоштовно, використовуючи функцію foreach і перебудову, а не змінюючи свій код.

  • foreach(item in list)конструкція дозволяє використовувати breakабо , continueякщо вам потрібно вийти ітерацію або цикл. Але ви не можете змінити список всередині циклу foreach.

Я здивований, побачивши, що list.ForEachце трохи швидше. Але це, мабуть, не вагомий привід використовувати його протягом усього часу, це була б передчасна оптимізація. Якщо ваша програма використовує базу даних або веб-сервіс, який, не керуючи циклами, майже завжди буде там, де йде час. І чи ти це також порівняв проти forциклу? Можливо, list.ForEachбуде швидше завдяки використанню цього внутрішнього forциклу, а цикл без обгортки буде ще швидшим.

Я не погоджуюся з тим, що list.ForEach(delegate)версія є «більш функціональною» у будь-якому значущому плані. Він передає функцію функції, але немає великої різниці в результатах чи організації програми.

Я не думаю, що foreach(item in list)"говорить точно так, як ви хочете, щоб це було зроблено" - for(int 1 = 0; i < count; i++)цикл робить це, foreachцикл залишає вибір контролю на компіляторі.

Моє відчуття - у новому проекті використовувати foreach(item in list)для більшості циклів, щоб дотримуватися загального використання та для читання, і використовувати list.Foreach()лише для коротких блоків, коли ви можете зробити щось більш елегантно чи компактно з =>оператором C # 3 " ". У таких випадках, можливо, вже існує метод розширення LINQ, який є більш конкретним, ніж ForEach(). Дивіться , якщо Where(), Select(), Any(), All(), Max()або один з багатьох інших методів LINQ вже не робити те , що ви хочете від петлі.


Просто для цікавості ... подивіться на впровадження Microsoft ... referenceource.microsoft.com/#mscorlib/system/collections/…
Олександр

17

Для розваги я спливав Список у рефлектор, і це C #:

public void ForEach(Action<T> action)
{
    if (action == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    for (int i = 0; i < this._size; i++)
    {
        action(this._items[i]);
    }
}

Аналогічно MoveNext у Enumerator, який використовується foreach, це:

public bool MoveNext()
{
    if (this.version != this.list._version)
    {
        ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
    }
    if (this.index < this.list._size)
    {
        this.current = this.list._items[this.index];
        this.index++;
        return true;
    }
    this.index = this.list._size + 1;
    this.current = default(T);
    return false;
}

Список.ForEach набагато більш скорочений, ніж MoveNext - набагато менша обробка - швидше за все JIT перетвориться на щось ефективне ..

Крім того, foreach () виділить нового Інвентера, незалежно від того. GC - твій друг, але якщо ти робиш один і той же передбачення неодноразово, це зробить більше предметів, що відкидаються, на відміну від повторного використання того ж делегата - АЛЕ - це справді облягаючий випадок. У типовому використанні ви побачите незначну або ніяку різницю.


1
Ви не маєте гарантії, що код, сформований foreach, буде однаковим між версіями компілятора. Створений код може бути вдосконалений майбутньою версією.
Ентоні

1
Станом на .NET Core 3.1 ForEach все ще швидший.
джекмот

13

Я знаю дві незрозумілі речі, які роблять їх різними. Іди мене!

По-перше, є класична помилка створення делегата для кожного елемента в списку. Якщо ви використовуєте ключове слово foreach, усі ваші делегати можуть посилатися на останній елемент списку:

    // A list of actions to execute later
    List<Action> actions = new List<Action>();

    // Numbers 0 to 9
    List<int> numbers = Enumerable.Range(0, 10).ToList();

    // Store an action that prints each number (WRONG!)
    foreach (int number in numbers)
        actions.Add(() => Console.WriteLine(number));

    // Run the actions, we actually print 10 copies of "9"
    foreach (Action action in actions)
        action();

    // So try again
    actions.Clear();

    // Store an action that prints each number (RIGHT!)
    numbers.ForEach(number =>
        actions.Add(() => Console.WriteLine(number)));

    // Run the actions
    foreach (Action action in actions)
        action();

У методу List.ForEach немає цієї проблеми. Поточний елемент ітерації передається за значенням як аргумент зовнішній лямбда, і тоді внутрішня лямбда правильно фіксує цей аргумент в його власному закритті. Проблема вирішена.

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

По-друге, підхід методу ForEach має обмеження. Якщо ви реалізуєте IEnumerable за допомогою повернення врожайності, ви не можете зробити повернення врожаю всередині лямбда. Таким чином, прокручування предметів у колекції для отримання повернутих речей цим методом неможливо. Вам доведеться використовувати ключове слово foreach і вирішити проблему закриття, зробивши вручну копію поточного значення циклу всередині циклу.

Більше тут


5
Проблема, про яку ви згадуєте, foreach- "виправлена" у C # 5. stackoverflow.com/questions/8898925/…
StriplingWarrior

13

Я думаю, що someList.ForEach()виклик може бути легко паралельний, тоді як нормальний foreachне є таким простим запуском паралелі. Ви можете легко запустити декілька різних делегатів на різних ядрах, що не так просто зробити з нормальним foreach.
Всього мої 2 копійки


2
Я думаю, він мав на увазі, що двигун виконання може паралелізувати його автоматично. Інакше, як foreach, так і .ForEach можна паралельно вручну використовувати нитку з пулу в кожному делегаті дії
Ісак Саво,

@Isak, але це було б неправильним припущенням. Якщо анонімний метод піднімає місцевого або члена, час виконання не буде 8) легко матимуть паралельних можливостей
Rune FS

6

Як кажуть, чорт у деталях ...

Найбільша різниця між двома методами перерахунку збору полягає в тому, що вони foreachнесуть стан, тоді як ForEach(x => { })ні.

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

Дозволяє використовувати List<T>в нашому маленькому експерименті для спостереження за поведінкою. Для цього експерименту я використовую .NET 4.7.2:

var names = new List<string>
{
    "Henry",
    "Shirley",
    "Ann",
    "Peter",
    "Nancy"
};

Дозволяє повторити це foreachспочатку:

foreach (var name in names)
{
    Console.WriteLine(name);
}

Ми можемо розширити це на:

using (var enumerator = names.GetEnumerator())
{

}

З перелічником у руці, заглядаючи під обкладинки, ми отримуємо:

public List<T>.Enumerator GetEnumerator()
{
  return new List<T>.Enumerator(this);
}
    internal Enumerator(List<T> list)
{
  this.list = list;
  this.index = 0;
  this.version = list._version;
  this.current = default (T);
}

public bool MoveNext()
{
  List<T> list = this.list;
  if (this.version != list._version || (uint) this.index >= (uint) list._size)
    return this.MoveNextRare();
  this.current = list._items[this.index];
  ++this.index;
  return true;
}

object IEnumerator.Current
{
  {
    if (this.index == 0 || this.index == this.list._size + 1)
      ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen);
    return (object) this.Current;
  }
}

Дві речі стають очевидними:

  1. Нам повертають надзвичайний предмет з інтимними знаннями основної колекції.
  2. Копія колекції - це неглибока копія.

Це, звичайно, жодним чином не безпечно для потоків. Як було зазначено вище, змінювати колекцію під час повторення - це просто погано моджо.

Але як бути з проблемою, коли колекція стає недійсною під час ітерації засобами поза нами, що спілкуються із колекцією під час ітерації? Кращі практики пропонують версію колекції під час операцій та ітерації та перевірку версій, щоб виявити, коли основна колекція змінюється.

Ось де все стає справді каламутним. Відповідно до документації Microsoft:

Якщо в колекцію вносяться зміни, такі як додавання, зміна або видалення елементів, поведінка перелічувача не визначена.

Ну, що це означає? В якості прикладу, лише тому, що List<T>реалізує обробку виключень, не означає, що всі колекції, які реалізують, IList<T>будуть робити те саме. Це здається явним порушенням принципу заміни Ліскова:

Об'єкти суперкласу повинні бути замінені об'єктами його підкласів, не порушуючи додаток.

Ще одна проблема полягає в тому, що перелічувач повинен реалізувати IDisposable- це означає ще одне джерело потенційних витоків пам’яті не тільки в тому випадку, якщо абонент помилився, але якщо автор не реалізує Disposeшаблон правильно.

Нарешті, у нас є питання про все життя ... що трапиться, якщо ітератор дійсний, але базової колекції немає? Зараз ми робимо короткий огляд того, що було ... коли ви розділяєте життя колекції та її ітераторів, ви задаєте собі проблеми.

Давайте тепер вивчимо ForEach(x => { }):

names.ForEach(name =>
{

});

Це поширюється на:

public void ForEach(Action<T> action)
{
  if (action == null)
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
  int version = this._version;
  for (int index = 0; index < this._size && (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5); ++index)
    action(this._items[index]);
  if (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
    return;
  ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}

Важливою увагою є наступне:

for (int index = 0; index < this._size && ... ; ++index) action(this._items[index]);

Цей код не виділяє жодних перелічників (нічим Dispose) і не робить пауз під час ітерації.

Зауважте, що це також виконує неглибоку копію базової колекції, однак тепер колекція - це знімок у часі. Якщо автор неправильно здійснив перевірку на те, чи збірка колекції змінюється або стає "несвіжою", знімок все ще діє.

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

Так, я сказав, що ітератори ... іноді вигідніше мати державу. Припустимо, ви хочете зберегти щось подібне до курсору бази даних ... можливо, шлях в декількох foreachстилях Iterator<T>. Мені особисто не подобається цей стиль дизайну, оскільки надто багато проблем у житті, і ви покладаєтесь на добру благодать авторів колекцій, на які ви покладаєтесь (якщо тільки ви буквально все не пишете з нуля).

Завжди є третій варіант ...

for (var i = 0; i < names.Count; i++)
{
    Console.WriteLine(names[i]);
}

Це не сексуально, але у нього зуби (вибачення Тома Круза і фільм Фірма )

Це ваш вибір, але тепер ви знаєте, і це може бути обізнаним.


5

Ви можете назвати анонімного делегата :-)

А друге ви можете написати так:

someList.ForEach(s => s.ToUpper())

Що я віддаю перевагу, і економить багато набравши текст.

Як каже Йоахім, паралелізм легше застосувати до другої форми.


2

За лаштунками анонімний делегат перетворюється на фактичний метод, так що ви можете мати деякий накладний витрата з другим вибором, якщо компілятор не вирішив вбудувати функцію. Крім того, будь-які локальні змінні, на які посилається тіло прикладу анонімного делегата, зміняться у природі через хитрощі компілятора, щоб приховати факт його компіляції до нового методу. Більше інформації тут про те, як C # робить цю магію:

http://blogs.msdn.com/oldnewthing/archive/2006/08/04/688527.aspx


2

Функція ForEach є членом списку загальних класів.

Я створив таке розширення для відтворення внутрішнього коду:

public static class MyExtension<T>
    {
        public static void MyForEach(this IEnumerable<T> collection, Action<T> action)
        {
            foreach (T item in collection)
                action.Invoke(item);
        }
    }

Отже, в кінці ми використовуємо звичайний передчуття (або цикл для, якщо хочете).

З іншого боку, використання функції делегата - це просто ще один спосіб визначення функції цього коду:

delegate(string s) {
    <process the string>
}

еквівалентно:

private static void myFunction(string s, <other variables...>)
{
   <process the string>
}

або використовуючи вирази labda:

(s) => <process the string>

2

Вся область ForEach (функція делегування) трактується як єдиний рядок коду (виклик функції), і ви не можете встановити точки перерви або перейти до коду. Якщо трапляється необроблений виняток, весь блок позначається.


2

List.ForEach () вважається більш функціональним.

List.ForEach()каже, що ви хочете зробити. foreach(item in list)також точно говорить, як ви хочете, щоб це було зроблено. Це залишає List.ForEachвільним змінити реалізацію того, як в майбутньому. Наприклад, гіпотетична майбутня версія .Net завжди може працювати List.ForEachпаралельно, за умови, що в цей момент у кожного є декілька процесорних ядер, які зазвичай сидять в режимі очікування.

З іншого боку, foreach (item in list)дає трохи більше контролю над циклом. Наприклад, ви знаєте, що елементи будуть повторені в якомусь послідовному порядку, і ви можете легко перерватися посередині, якщо предмет відповідає якійсь умові.


Деякі новітні зауваження щодо цього питання доступні тут:

https://stackoverflow.com/a/529197/3043


1

Другий спосіб, який ви показали, використовує метод розширення для виконання методу делегування для кожного з елементів у списку.

Таким чином, у вас є інший виклик делегата (= метод).

Крім того, є можливість повторити список за допомогою циклу for .


1

Варто бути обережним, як вийти з методу Generic .ForEach - див. Цю дискусію . Хоча посилання, схоже, говорить про те, що цей шлях найшвидший. Не впевнений, чому - ви могли б подумати, що після їх складання вони будуть рівнозначними ...


0

Є спосіб, я це зробив у своєму додатку:

List<string> someList = <some way to init>
someList.ForEach(s => <your actions>);

Ви можете використовувати як свій предмет з foreach

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