Як кажуть, чорт у деталях ...
Найбільша різниця між двома методами перерахунку збору полягає в тому, що вони 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]);
}
Це не сексуально, але у нього зуби (вибачення Тома Круза і фільм Фірма )
Це ваш вибір, але тепер ви знаєте, і це може бути обізнаним.