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цикл для итерации коллекции, это допустимо (edit: до .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) вместо этого
Mark Cidade

2
С Linq все можно сделать лучше. Я только что показал пример изменения коллекции внутри foreach ...

2
RemoveAll () - это метод в List <T>.
Марк Сидаде

3
Вы, вероятно, знаете об этом, но люди должны остерегаться удаления предметов таким образом; если вы удалите элемент N, то итерация пропустит элемент (N + 1), и вы не увидите его в своем делегате или не получите возможность удалить его, как если бы вы сделали это в своем цикле for.
Эль Зорко

9
Если вы перебираете список в обратном направлении с помощью цикла for, вы можете удалять элементы без проблем смещения индекса.
С. Тарык Четин

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 предпочтительнее других циклов, таких как циклы for или while, - главное, что вы позволяете компилятору выбирать лучший способ построения петля. Если будущей версии компилятора удастся использовать более быстрый метод, вы получите это бесплатно, используя 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

Ради интереса, я вставил List в отражатель, и в результате получился 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;
}

List.ForEach гораздо более урезан, чем MoveNext - гораздо меньше обработки - с большей вероятностью превратит JIT во что-то эффективное.

Кроме того, foreach () будет выделять новый перечислитель, несмотря ни на что. GC - ваш друг, но если вы неоднократно выполняете один и тот же foreach, это создаст больше одноразовых объектов, в отличие от повторного использования одного и того же делегата - НО - это действительно незначительный случай. При обычном использовании вы увидите мало или нет разницы.


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 является членом List, а не методом расширения, хотя его легко определить самостоятельно, поэтому у вас есть эта возможность для любого перечислимого типа.)

Во-вторых, подход метода ForEach имеет ограничение. Если вы реализуете IEnumerable с использованием yield return, вы не сможете выполнить возврат дохода внутри лямбды. Таким образом, циклически просматривая элементы в коллекции для получения возвращаемых вещей, этот метод невозможен. Вам нужно будет использовать ключевое слово foreach и обойти проблему закрытия, вручную сделав копию текущего значения цикла внутри цикла.

Больше здесь


5
Проблема, о которой вы упоминаете, foreach«исправлена» в C # 5. stackoverflow.com/questions/8898925/…
StriplingWarrior

13

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


2
Я думаю, что он имел в виду, что двигатель времени выполнения мог бы распараллеливать его автоматически. В противном случае и foreach, и .ForEach можно распараллелить вручную, используя поток из пула в каждом делегате действия
Исак Саво,

@ Исак, но это было бы неверным предположением. Если анонимный метод поднимает локальный или член, среда выполнения будет нелегко) сможет параллелизироваться
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]);
}

Это не сексуально, но у него есть зубы (извинения Тому Крузу и фильму The Firm )

Это ваш выбор, но теперь вы знаете, и это может быть осознанным.


5

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

И вы можете написать второй как:

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

Который я предпочитаю, и экономит много печатать.

Как говорит Иоахим, параллелизм легче применить ко второй форме.


2

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

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


2

Функция ForEach является членом универсального класса List.

Я создал следующее расширение для воспроизведения внутреннего кода:

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

Итак, в конце мы используем обычный foreach (или цикл, если хотите).

С другой стороны, использование функции делегата - это просто еще один способ определить функцию, этот код:

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

Остерегайтесь того, как выйти из универсального метода .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.