Как говорится, дьявол кроется в деталях ...
Самое большое различие между этими двумя методами перечисления состоит в том, что он 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;
}
}
Две вещи становятся очевидными:
- Нам возвращается объект с состоянием и глубоким знанием основной коллекции.
- Копия коллекции является мелкой копией.
Это, конечно, никоим образом не безопасно. Как указывалось выше, изменение коллекции во время итерации - просто плохое моджо.
Но как насчет проблемы того, чтобы коллекция стала недействительной во время итерации с помощью средств, не связанных с нами во время итерации? В соответствии с рекомендациями рекомендуется создавать версии коллекции во время операций и итерации, а также проверять версии, чтобы определить, когда базовая коллекция изменяется.
Здесь вещи становятся действительно темными. Согласно документации 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 )
Это ваш выбор, но теперь вы знаете, и это может быть осознанным.