Почему в IEnumerable отсутствует метод расширения ForEach?


389

Вдохновлен еще одним вопросом о недостающей Zipфункции:

Почему ForEachв Enumerableклассе нет метода расширения ? Или где угодно? Единственный класс, который получает ForEachметод List<>. Есть ли причина, по которой он отсутствует (производительность)?


Вероятно, как уже было сказано в предыдущем посте, в этом foreach поддерживается ключевое слово языка в C # и VB.NET (и многих других). Большинство людей (включая меня) просто пишут их сами по мере необходимости и необходимости.
Крис

2
Похожий вопрос здесь со ссылкой на «официальный ответ» stackoverflow.com/questions/858978/…
Benjol


Я думаю, что один очень важный момент заключается в том, что цикл foreach легче обнаружить. Если вы используете .ForEach (...), легко пропустить этот, когда вы просматриваете код. Это становится важным, когда у вас есть проблемы с производительностью. Конечно .ToList () и .ToArray () имеют одинаковую проблему, но они используются немного по-разному. Другая причина может заключаться в том, что в .ForEach более распространено изменение исходного списка (например, удаление / добавление элементов), чтобы он больше не был «чистым» запросом.
Даниэль Бишар

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

Ответы:


198

Уже есть выражение foreach, включенное в язык, который выполняет работу большую часть времени.

Я бы не хотел видеть следующее:

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

Вместо:

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

Последний в большинстве случаев более понятен и удобен для чтения , хотя, возможно, он печатается немного дольше.

Однако я должен признать, что изменил свою позицию по этому вопросу; метод расширения ForEach () действительно может быть полезен в некоторых ситуациях.

Вот основные различия между оператором и методом:

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

Это все замечательные замечания, сделанные многими людьми здесь, и я понимаю, почему людям не хватает этой функции. Я не против, чтобы Microsoft добавила стандартный метод ForEach в следующей итерации фреймворка.


39
Обсуждение здесь дает ответ: forums.microsoft.com/MSDN/… По сути, было принято решение сохранить функциональные методы расширения "чистыми". ForEach будет стимулировать побочные эффекты при использовании методов расширения, что не является целью.
Манкаус

17
«foreach» может показаться более понятным, если у вас есть C-фон, но внутренняя итерация более четко определяет ваше намерение. Это означает, что вы можете делать такие вещи, как sequence.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 и не допускать его столкновения с методом List <>. ForEach.
Кэмерон МакФарланд,

3
Кэмерон, поверь мне, я знаю, как работают методы расширения :) Список наследует IEnumerable, так что это будет неочевидно.
Ака

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

3
Я говорю что-то другое? Я думаю, что это плохо, когда многие классы имеют метод ForEach, но некоторые из них могут работать по-разному.
Ака

5
Одна интересная вещь о List <>. 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;
    }
}

Pros

Позволяет цепочки:

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

Cons

Это на самом деле ничего не будет делать, пока вы не сделаете что-то, чтобы вызвать итерацию. По этой причине его не следует называть .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;}
jjnguy

3
Не могли бы вы просто использовать Select вместо метода Apply? Мне кажется, что применение действия к каждому элементу и его получение - это именно то, что делает Select. Напримерnumbers.Select(n => n*2);
rasmusvhansen

1
Я думаю, что Apply более читабельно, чем использование Select для этой цели. Когда я читаю оператор Select, я читаю его как «получить что-то изнутри», где «Применить» означает «выполнить какую-то работу».
Доктор Роб Ланг

2
@rasmusvhansen На самом деле, Select()говорит, применить функцию к каждому элементу и вернуть значение функции, где Apply()говорит применить Actionк каждому элементу и вернуть исходный элемент.
NetMage

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

35

Обсуждение здесь дает ответ:

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

В основном было принято решение сохранить функциональные методы расширения "чистыми". ForEach будет стимулировать побочные эффекты при использовании методов расширения Enumerable, что не является целью.


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

7
Это рассуждение полностью поддельное. Сценарий использования заключается в том, что необходимы побочные эффекты ... без ForEach вы должны написать цикл 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

Отличное расширение, но не могли бы вы добавить документацию? Каково предполагаемое использование возвращаемого значения? Почему index var, а не int? Пример использования?
Марк Т

Отметка: возвращаемое значение - количество элементов в списке. Индекс является специально набран как межды, благодаря неявной типизации.
Крис Цвирик

@Chris, на основе приведенного выше кода возвращаемое значение - это индекс с нуля последнего значения в списке, а не количество элементов в списке.
Эрик Смит

@ Крис, нет, это не так: индекс увеличивается после того, как значение было взято (постфиксный инкремент), поэтому после обработки первого элемента индекс будет равен 1 и так далее. Таким образом, возвращаемое значение действительно является количеством элементов.
MartinStettner

1
@MartinStettner комментарий Эрика Смита 5/16 был от более ранней версии. Он исправил код 5/18, чтобы вернуть правильное значение.
Майкл Блэкберн

16

Один из способов это написать .ToList().ForEach(x => ...).

профи

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

Синтаксический шум очень мягкий (только добавляет немного постороннего кода).

Обычно не стоит дополнительной памяти, так как в .ForEach()любом случае нативу придется реализовать всю коллекцию.

минусы

Порядок операций не идеален. Я предпочел бы реализовать один элемент, затем действовать на него, а затем повторить. Этот код сначала реализует все элементы, а затем воздействует на них по очереди.

Если реализация списка вызывает исключение, вы никогда не сможете воздействовать на один элемент.

Если перечисление бесконечно (как натуральные числа), вам не повезло.


1
а) Это даже не отдаленный ответ на вопрос. б) Этот ответ был бы более понятным, если бы он упомянул заранее, что, хотя нет Enumerable.ForEach<T>метода, есть List<T>.ForEachметод. c) «Обычно не стоит дополнительной памяти, поскольку нативный .ForEach () в любом случае должен был бы реализовать всю коллекцию». - полная чушь. Вот реализация 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 может быть нулевым ... вы также можете это проверить.
Эми Б

Да, я проверяю свою библиотеку, я просто пропустил это, когда делал это быстро там.
Аарон Пауэлл

Мне интересно, что все проверяют параметр action на null, но никогда не указывают параметр source / col.
Кэмерон Макфарланд

2
Мне интересно, что все проверяют параметры на null, а также не проверяют результаты в исключении ...
oɔɯǝɹ

Не за что. Исключение Null Ref не сообщит вам имя параметра, который был ошибочным. И вы могли бы также включить в цикл, чтобы вы могли выдать конкретный пустой указатель, говоря, что col [index #] был нулевым по той же причине
Роджер Уиллкокс

8

Поэтому было много комментариев о том, что метод расширения ForEach не подходит, потому что он не возвращает значение, подобное методам расширения LINQ. Хотя это фактическое утверждение, оно не совсем верно.

Все методы расширения LINQ возвращают значение, поэтому их можно объединить в цепочку:

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

Однако то, что LINQ реализован с использованием методов расширения, не означает, что методы расширения должны использоваться одинаково и возвращать значение. Написание метода расширения для предоставления общих функций, которые не возвращают значение, является вполне допустимым использованием.

Конкретный аргумент о ForEach заключается в том, что, основываясь на ограничениях на методы расширения (а именно на том, что метод расширения никогда не будет переопределять унаследованный метод с той же сигнатурой ), может возникнуть ситуация, когда пользовательский метод расширения доступен во всех классах, которые реализуют IEnumerable <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))который фактически эквивалентен foreach, но ленив и цепочек.


Фантастически, он никогда не нажимал, что возвращенный выбор будет работать так. Хорошее использование синтаксиса метода, так как я не думаю, что вы могли бы сделать это с синтаксисом выражения (следовательно, я не думал об этом раньше).
Алекс КейСмит

@AlexKeySmith Вы могли бы сделать это с синтаксисом выражения, если бы в C # был оператор последовательности, такой как его предшественник. Но весь смысл ForEach в том, что он выполняет действие с типом void, и вы не можете использовать типы void в выражениях в C #.
Джим Балтер

imho, теперь Selectон больше не делает то, что должен делать. это определенно решение для того, о чем просит OP - но в отношении читабельности темы: никто не ожидал бы, что select выполняет функцию.
Матиас Бургер

@MatthiasBurger Если Selectне делать то, что он должен делать, это проблема с реализацией Select, а не с кодом, который вызывает Select, который не влияет на то, что Selectделает.
Мартейн


4

@Coincoin

Реальная сила метода расширения foreach заключается в возможности повторного использования Action<>без добавления ненужных методов в ваш код. Скажем, у вас есть 10 списков, и вы хотите выполнять с ними одинаковую логику, а соответствующая функция не вписывается в ваш класс и не используется повторно. Вместо десяти циклов for или универсальной функции, которая, очевидно, является вспомогательным и не принадлежит, вы можете хранить всю свою логику в одном месте ( 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 ...}
Кэмерон МакФарланд,

Если это так просто , как f(p), затем оставить вне { }для стенографии: for (var p in List1.Concat(List2).Concat(List2)) f(p);.
Спулсон

3

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


2
ForEach использует Action <T>, что является еще одной формой делегата
Аарон Пауэлл,

2
За исключением права Леппи, все методы расширения Enumerable возвращают значение, поэтому ForEach не соответствует шаблону. Делегаты не входят в это.
Кэмерон Макфарланд,

Просто метод расширения не должен возвращать результат, он служит для добавления способа вызова часто используемой операции
Аарон Пауэлл,

1
Действительно, Слейс. Так что, если он не возвращает значение?
TraumaPony

1
@Martijn iepublic 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, потому что он не будет чисто функциональным; тем не менее, они ДАЛИ (назовите его) на более функциональном языке, 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, который заменяет этот билет? MS
Connect

Это не было реализовано. Подумайте о том, чтобы подать новый выпуск на github.com/dotnet/runtime/issues
Кирилл Осенков


2

Это я или это List <T>. Потому что Линк почти устарел. Первоначально был

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 мы можем вместо этого использовать лямбду для того же эффекта, например, попробуйте http://dotnet-developments.blogs.techtarget.com/2008/09/02/iterators-lambda-and-linq-oh- мой /


1
Я считаю, что сгенерированный код для foreach должен иметь блок try-finally. Потому что IEnumerator от T наследуется от интерфейса IDisposable. Итак, в сгенерированном блоке finally, 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 проверяется во время выполнения.

Сделав некоторый рефакторинг, где оба метода использовались в коде, я предпочитаю .ForEach, так как мне приходилось выслеживать сбои тестов / сбои во время выполнения, чтобы найти проблемы foreach.


5
Это действительно так? Я не вижу, как foreachможно меньше проверять время компиляции. Конечно, если ваша коллекция не является универсальной IEnumerable, переменная цикла будет object, но то же самое будет верно для ForEachметода гипотетического расширения.
Ричард Пул

1
Это упоминается в принятом ответе. Тем не менее, это просто неправильно (и Ричард Пул выше, правильно).
Джим Балтер
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.