Чому на IEnumerable не існує методу розширення ForEach?


389

Натхненний ще одним запитанням про відсутність Zipфункції:

Чому ForEachв Enumerableкласі немає методу розширення ? Або де завгодно? Єдиний клас, який отримує ForEachметод, - це List<>. Чи є причина, чому вона відсутня (виконання)?


Ймовірно, як натякає попередня публікація, в тому, що передбачення підтримується як мовне ключове слово на C # і VB.NET (та багатьох інших) Більшість людей (включаючи і мене) просто пишуть їх самі по мірі необхідності та відповідності.
chrisb

2
Пов'язані питання тут з посиланням на «офіційну відповідь» stackoverflow.com/questions/858978 / ...
Benjol

1
Дивіться також stackoverflow.com/questions/200574 / ...
goodeye

Я думаю, що дуже важливим моментом є те, що петлю переднього плану легше помітити. Якщо ви використовуєте .ForEach (...), це легко пропустити під час перегляду коду. Це стає важливим, коли у вас є проблеми з продуктивністю. Звичайно .ToList () і .ToArray () мають одну проблему, але вони використовуються трохи по-іншому. Іншою причиною може бути те, що в .ForEach частіше змінюється список джерел (fe видалення / додавання елементів), щоб він більше не був "чистим" запитом.
Даніель Бішар

Під час налагодження паралелізованого коду я часто намагаюся поміняти місцями лише Parallel.ForEachдля Enumerable.ForEachтого, щоб знову виявити останній. C # пропустив трюк, щоб полегшити тут справи.
Полковник Паніка

Відповіді:


198

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

Мені б не хотілося бачити наступне:

list.ForEach( item =>
{
    item.DoSomething();
} );

Замість:

foreach(Item item in list)
{
     item.DoSomething();
}

Останнє чіткіше і легше читати в більшості ситуацій , хоча, можливо, трохи довше вводити текст.

Однак, маю визнати, я змінив свою позицію з цього приводу; Метод розширення ForEach () дійсно був би корисним у деяких ситуаціях.

Ось основні відмінності між оператором та методом:

  • Перевірка типу: foreach робиться під час виконання, ForEach () знаходиться під час компіляції (Big Plus!)
  • Синтаксис виклику делегата дійсно набагато простіший: object.ForEach (DoSomething);
  • ForEach () може бути прикутий: хоча злість / корисність такої функції є відкритою для обговорення.

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


39
Тут обговорення дає відповідь: forums.microsoft.com/MSDN/… В основному було прийнято рішення зберегти методи розширення функціонально "чистими". A ForEach заохочував би побічні ефекти при використанні методів розширення, що не було наміром.
манкус

17
"foreach" може здатися більш зрозумілим, якщо у вас є C, але внутрішня ітерація чіткіше говорить про ваші наміри. Це означає, що ви можете робити такі речі, як "послідовність.AsParallel (). ForEach (...)".
Джей Базузі

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

49
Метод розширення буде дуже корисно в ланцюзі LINQ з допомогою точкової нотації, як: col.Where(...).OrderBy(...).ForEach(...). Набагато простіший синтаксис, без забруднення екрана або надмірними локальними змінними, або довгими дужками в foreach ().
Яцек Горгонь

22
"Мені б не хотілося бачити наступне" - Питання не стосувалося ваших особистих уподобань, і ви зробили код більш ненависним, додавши сторонні канали ліній і сторонні пари брекетів. Не така ненависна list.ForEach(item => item.DoSomething()). І хто каже, що це список? Очевидний випадок використання знаходиться в кінці ланцюга; з заявою foreach, ви повинні будете поставити весь ланцюжок після in, а операцію, виконану всередині блоку, що набагато менш природно або послідовно.
Джим Балтер

73

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

Однак якщо ви справді не вистачаєте цієї маленької приємної функції, ви можете розгорнути свою власну версію

public static void ForEach<T>(
    this IEnumerable<T> source,
    Action<T> action)
{
    foreach (T element in source) 
        action(element);
}

11
Методи розширення дозволяють це зробити. Якщо вже існує реалізація екземпляра заданого імені методу, тоді цей метод використовується замість методу розширення. Таким чином, ви можете реалізувати метод розширення ForEach і не стикатися з методом Список <>.
Камерон Макфарланд

3
Кемерон, повірте мені, я знаю, як працюють методи розширення :) Список успадковує IEumerable, так що це буде не очевидно.
аку

9
З посібника з програмування методів розширення ( msdn.microsoft.com/en-us/library/bb383977.aspx ): метод розширення з тим самим іменем та підписом, як інтерфейс чи метод класу, ніколи не буде викликаний. Мені це здається досить очевидним.
Камерон Макфарланд

3
Чи я кажу про щось інше? Я думаю, що це не добре, коли багато класів мають метод ForEach, але деякі з них можуть працювати інакше.
аку

5
Одна цікава річ, яку слід зазначити про Список <>. ForEach та Array.ForEach, це те, що вони насправді не використовують внутрішню конструкцію foreach. Вони обидва використовують рівнину для петлі. Можливо, це річ про продуктивність ( diditwith.net/2006/10/05/PerformanceOfForeachVsListForEach.aspx ). Але враховуючи це, що Array і List реалізують методи ForEach, дивно, що вони принаймні не реалізували метод розширення для IList <>, якщо не для IEnumerable <>.
Метт Дотсон

53

Ви можете написати цей метод розширення:

// Possibly call this "Do"
IEnumerable<T> Apply<T> (this IEnumerable<T> source, Action<T> action)
{
    foreach (var e in source)
    {
        action(e);
        yield return e;
    }
}

Плюси

Дозволяє ланцюжок:

MySequence
    .Apply(...)
    .Apply(...)
    .Apply(...);

Мінуси

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

// possibly call this "Realize"
IEnumerable<T> Done<T> (this IEnumerable<T> source)
{
    foreach (var e in source)
    {
        // do nothing
        ;
    }

    return source;
}

Це може бути занадто значним відхиленням від бібліотек C # для доставки; читачі, які не знайомі з вашими методами розширення, не знають, що робити з вашим кодом.


1
Ваша перша функція може бути використана без методу ' {foreach (var e in source) {action(e);} return source;}
real

3
Не могли ви просто використати Select замість методу застосувати? Мені здається, що застосувати дію до кожного елемента і поступитися ним саме те, що робить Select. Напр.numbers.Select(n => n*2);
rasmusvhansen

1
Я думаю, що застосувати є більш зрозумілим, ніж використовувати для цієї мети Select. Читаючи твердження Select, я читаю це як «дістати щось ізсередини», де «Застосувати» - «виконати якусь роботу».
Доктор Роб Ланг

2
@rasmusvhansen Власне, Select()говорить застосувати функцію до кожного елемента та повернути значення функції, де Apply()сказати застосувати a Actionдо кожного елемента та повернути початковий елемент.
NetMage

1
Це еквівалентноSelect(e => { action(e); return e; })
Джим Балтер

35

Тут обговорення дає відповідь:

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

В основному було прийнято рішення зберегти методи розширення функціонально "чистими". A ForEach буде заохочувати побічні ефекти при використанні методів розширення безлічі, що не було наміром.


6
Дивіться також blogs.msdn.com/b/ericlippert/archive/2009/05/18 / ... . Ddoing настільки порушує принципи функціонального програмування, на яких ґрунтуються всі інші оператори послідовності. Очевидно, що єдиною метою заклику до цього методу є побічні ефекти
Майкл Фрейджім

7
Це міркування є абсолютно хибним. Випадок використання є те , що побічні ефекти необхідні ... без ForEach, ви повинні написати цикл Еогеасп. Існування ForEach не заохочує побічні ефекти, а його відсутність не зменшує їх.
Джим Балтер

Зважаючи на те, як просто написати метод розширення, буквально немає причин, що це не стандарт мови.
Капітан Принні

17

Хоча я згоден, що foreachв більшості випадків краще використовувати вбудовану конструкцію, я вважаю, що використання цієї зміни в розширенні ForEach <> трохи приємніше, ніж самостійно керувати індексом foreach:

public static int ForEach<T>(this IEnumerable<T> list, Action<int, T> action)
{
    if (action == null) throw new ArgumentNullException("action");

    var index = 0;

    foreach (var elem in list)
        action(index++, elem);

    return index;
}
Приклад
var people = new[] { "Moe", "Curly", "Larry" };
people.ForEach((i, p) => Console.WriteLine("Person #{0} is {1}", i, p));

Дамо вам:

Person #0 is Moe
Person #1 is Curly
Person #2 is Larry

Відмінне розширення, але ви могли б додати трохи документації? Яке цільове використання повернутого значення? Чому індекс var, а не конкретно, int? Використання зразка?
Марк Т

Позначити: повернене значення - це кількість елементів у списку. Індекс є спеціально набраний як між, завдяки неявній типізації.
Кріс Цвірик

@Chris, виходячи з вашого коду вище, значення повернення - це нульовий індекс останнього значення у списку, а не кількість елементів у списку.
Ерік Сміт

@Chris, ні, це не так: індекс збільшується після прийняття значення (приріст постфіксу), тому після обробки першого елемента індекс буде 1 і так далі. Таким чином, повернене значення дійсно є числом елементів.
MartinStettner

1
@MartinStettner коментар Еріка Сміта 5/16 був з попередньої версії. Він виправив код на 5/18, щоб повернути правильне значення.
Майкл Блекберн

16

Одне вирішення - писати .ToList().ForEach(x => ...).

плюси

Легко зрозуміти - читачеві потрібно лише знати, що постачається з C #, а не якимись додатковими методами розширення.

Синтаксичний шум дуже м'який (лише додає трохи вигідного коду).

Зазвичай це не коштує додаткової пам’яті, оскільки рідному .ForEach()доведеться реалізувати всю колекцію.

мінуси

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

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

Якщо перерахунок нескінченний (як натуральні числа), вам не пощастило.


1
а) Це навіть не віддалена відповідь на питання. б) Ця відповідь була б зрозумілішою, якби вона наперед сказала, що, хоча немає Enumerable.ForEach<T>методу, існує List<T>.ForEachметод. c) "Зазвичай це не коштує додаткової пам'яті, оскільки рідному .OREach () в будь-якому випадку доведеться реалізувати всю колекцію." - повна і сувора дурниця. Ось реалізація програми Enumerable.ForEach <T>, яку C # надасть, якби це зробив:public static void ForEach<T>(this IEnumerable<T> list, Action<T> action) { foreach (T item in list) action(item); }
Джим Балтер,

13

Мені завжди було цікаво, що я сам, тому я завжди ношу це з собою:

public static void ForEach<T>(this IEnumerable<T> col, Action<T> action)
{
    if (action == null)
    {
        throw new ArgumentNullException("action");
    }
    foreach (var item in col)
    {
        action(item);
    }
}

Хороший маленький метод розширення.


2
col може бути нульовим ... ви можете це також перевірити.
Емі Б,

Так, я перевіряю в своїй бібліотеці, я просто пропустив це, коли це швидко робив там.
Аарон Пауелл

Мені цікаво, що кожен перевіряє параметр дії на null, але ніколи не є параметром source / col.
Камерон Макфарланд

2
Мені цікаво, що всі перевіряють параметри на null, а також не перевіряючи результати за винятком ...
жовтня 1313

Зовсім ні. Виняток Null Ref не вказуватиме вам ім'я параметра, який стався за помилкою. І ви також можете перевірити в циклі, щоб ви могли кинути конкретний Null Ref, кажучи, що col [індекс #] був недійсним з тієї ж причини
Роджер Уіллок

8

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

Усі методи розширення LINQ повертають значення, щоб вони могли бути пов'язані між собою:

collection.Where(i => i.Name = "hello").Select(i => i.FullName);

Однак те, що LINQ реалізовано за допомогою методів розширення, не означає, що методи розширення повинні використовуватися однаково і повертати значення. Написання методу розширення для викриття загальної функціональності, яка не повертає значення, є цілком правильним використанням.

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


7

Ви можете скористатися (прийнятним, але ліниво оціненим) Select, спочатку зробивши свою операцію, а потім повернувши ідентифікацію (або щось інше, якщо вам зручніше)

IEnumerable<string> people = new List<string>(){"alica", "bob", "john", "pete"};
people.Select(p => { Console.WriteLine(p); return p; });

Вам потрібно буде переконатися, що вона все ще оцінюється, або з Count()(найдешевша операція для перерахування afaik), або з іншою операцією, яка вам потрібна.

Я хотів би, щоб це було внесено до стандартної бібліотеки:

static IEnumerable<T> WithLazySideEffect(this IEnumerable<T> src, Action<T> action) {
  return src.Select(i => { action(i); return i; } );
}

Вищенаведений код стає таким, people.WithLazySideEffect(p => Console.WriteLine(p))який фактично еквівалентний передчуттю, але ледачий і вигідний.


Фантастично, він ніколи не клацав, що повернений вибір буде працювати так. Гарне використання синтаксису методу, тому що я не думаю, що ви могли це зробити із синтаксисом виразів (отже, я раніше про це не думав).
Alex KeySmith

@AlexKeySmith Ви можете зробити це з синтаксисом вираження, якби C # мав оператор послідовності, як його передній. Але вся суть ForEach полягає в тому, що він виконує дії з недійсним типом, і ти не можеш використовувати типи пустот у виразах у C #.
Джим Балтер

Імхо, тепер Selectбільше не робить того, що повинен робити. це, безумовно, рішення для того, що задає ОП - але щодо читабельності теми: ніхто не сподівався, що вибір виконує функцію.
Маттіас Бургер

@MatthiasBurger Якщо Selectне робити те, що повинен зробити, це проблема з реалізацією Select, а не з кодом, який викликає Select, який не впливає на те, що Selectробить.
Мартейн


4

@Coincoin

Реальна сила методу розширення foreach включає повторне використання Action<>без додавання зайвих методів до вашого коду. Скажіть, що у вас є 10 списків і ви хочете виконувати ту саму логіку, і відповідна функція не вписується у ваш клас і не використовується повторно. Замість того, щоб мати десять циклів або загальну функцію, яка, очевидно, є помічником, який не належить, ви можете зберегти всю свою логіку в одному місці ( Action<>. Отже, десятки рядків заміняються на

Action<blah,blah> f = { foo };

List1.ForEach(p => f(p))
List2.ForEach(p => f(p))

тощо ...

Логіка в одному місці, і ви не забруднили свій клас.


foreach (var p в List1.Concat (List2) .Concat (List3)) {... do stuff ...}
Cameron MacFarland

Якщо це так просто , як f(p), потім залишити поза { }для стенографії: for (var p in List1.Concat(List2).Concat(List2)) f(p);.
подружжя

3

Більшість методів розширення LINQ повертають результати. ForEach не вписується в цей шаблон, оскільки він нічого не повертає.


2
ForEach використовує Action <T>, що є ще однією формою делегата
Аарон Пауелл,

2
За винятком права леппі, всі перелічені методи розширення повертають значення, тому ForEach не відповідає шаблону. Делегати не вступають у це.
Камерон Макфарланд

Тільки те, що метод розширення не повинен повертати результат, він служить метою додавання способу виклику часто використовуваної операції
Аарон Пауелл,

1
Справді, Слейче. Що робити, якщо воно не повертає значення?
TraumaPony

1
@Martijn тобтоpublic IEnumerable<T> Foreach<T>(this IEnumerable<T> items, Action<T> action) { /* error check */ foreach (var item in items) { action(item); yield return item; } }
Маккей

3

Якщо у вас є F # (який буде в наступній версії .NET), ви можете використовувати

Seq.iter doSomething myIEnumerable


9
Тому Microsoft вирішила не надавати метод ForEach для IEnumerable у C # та VB, оскільки це не було б суто функціональним; все ж вони DID надають його (ім'я імені) на своїй більш функціональній мові, F #. Їх логіка натякає на мене.
Скотт Хатчінсон

3

Частково це відбувається тому, що мовні дизайнери не згодні з цим з філософської точки зору.

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

https://blogs.msdn.microsoft.com/ericlippert/2009/05/18/foreach-vs-foreach/


2

Ви можете використовувати select, коли хочете щось повернути. Якщо цього немає, спочатку ви можете скористатися ToList, оскільки ви, мабуть, не хочете нічого змінювати в колекції.


а) Це не відповідь на запитання. б) ToList потребує додаткового часу та пам'яті.
Джим Балтер

2

Я написав про це запис у блозі: http://blogs.msdn.com/kirillosenkov/archive/2009/01/31/foreach.aspx

Ви можете проголосувати тут, якщо хочете бачити цей метод у .NET 4.0: http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=279093


це коли-небудь реалізовувалося? Я думаю, що ні, але чи є інша URL-адреса, яка замінює цей квиток?
З'єднання з

Це не було реалізовано. Розгляньте можливість подати нове видання на github.com/dotnet/runtime/isissue
Кирило Осенков


2

Це я чи це Список <T> .Багато більше, Linq зробив застарілим. Спочатку був

foreach(X x in Y) 

де Y просто повинен бути IEnumerable (Pre 2.0) та реалізувати GetEnumerator (). Якщо ви подивитесь на створений MSIL, ви можете побачити, що він точно такий же, як

IEnumerator<int> enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
    int i = enumerator.Current;

    Console.WriteLine(i);
}

(Дивіться http://alski.net/post/0a-for-foreach-forFirst-forLast0a-0a-.aspx для MSIL)

Потім у DotNet2.0 Generics прийшли разом і Список. Foreach завжди вважав мене втіленням схеми Vistor (див. Шаблони дизайну від Gamma, Helm, Johnson, Vlissides).

Тепер, звичайно, у 3.5 ми можемо замість цього використовувати Lambda з тим же ефектом, наприклад, спробуйте http://dotnet-developments.blogs.techtarget.com/2008/09/02/iterators-lambda-and-linq-oh- мій /


1
Я вважаю, що згенерований код для "foreach" повинен мати блок "спроб остаточного". Тому що IEnumerator of T успадковує інтерфейс IDisposable. Отже, в згенерованому остаточному блоці IEnumerator з екземпляра T повинен розпоряджатися.
Морган Ченг

2

Я хотів би розширити відповідь Аку .

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

private static IEnumerable<T> ForEach<T>(IEnumerable<T> xs, Action<T> f) {
    foreach (var x in xs) {
        f(x); yield return x;
    }
}

1
Це те саме, щоSelect(x => { f(x); return x;})
Джим Балтер

0

Ніхто ще не вказував, що ForEach <T> приводить до перевірки типу компіляції часу, де перевірено ключове слово foreach.

Зробивши рефакторинг, де обидва способи використовувались у коді, я віддаю перевагу. Для кожного, оскільки мені довелося розшукувати тести / збої в роботі, щоб знайти проблеми передбачення.


5
Це справді так? Я не бачу, як foreachпроводиться перевірка часу компіляції. Звичайно, якщо ваша колекція не є загальним IEnumerable, змінна цикл буде an object, але те ж саме було б і для гіпотетичного ForEachметоду розширення.
Річард Пул

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