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

309

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

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

  • 0
    Вероятно, как и указывалось в предыдущем посте, в этом foreach поддерживается как ключевое слово языка в C # и VB.NET (и многих других). Большинство людей (включая меня) просто пишут их сами по мере необходимости и необходимости.
  • 2
    Связанный вопрос здесь со ссылкой на «официальный ответ» stackoverflow.com/questions/858978/…
Показать ещё 7 комментариев
Теги:
extension-methods

20 ответов

177
Лучший ответ

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

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

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

Вместо:

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

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

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

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

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

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

  • 0
    Хм, хорошая мысль. Единственное, что я могу подумать об этих счетчиках, это тот факт, что List <> также имеет метод ForEach. К сожалению, я не могу вспомнить пример, где наличие ForEach было бы полезно синтаксически.
  • 0
    Но что произойдет, если у вас уже есть делегат? :)
Показать ещё 20 комментариев
68

Для LINQ был добавлен метод ForEach. Если вы добавите расширение ForEach, он никогда не будет вызван для экземпляров List из-за ограничений методов расширения. Я думаю, что причина, по которой он не был добавлен, - это не помеха с существующим.

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

public static void ForEach<T>(
    this IEnumerable<T> source,
    Action<T> action)
{
    foreach (T element in source) 
        action(element);
}
  • 8
    Методы расширения позволяют это сделать. Если уже существует реализация экземпляра данного имени метода, тогда этот метод используется вместо метода расширения. Таким образом, вы можете реализовать метод расширения ForEach и не допускать его столкновения с методом List <>. ForEach.
  • 2
    Кэмерон, поверь мне, я знаю, как работают методы расширения :) Список наследует IEnumerable, так что это будет неочевидно.
Показать ещё 6 комментариев
48

Вы можете написать этот метод расширения:

// 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(...);

против

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

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

    return source;
}

Это может быть слишком значительным отход от библиотек С# доставки; читатели, которые не знакомы с вашими методами расширения, не будут знать, что делать с вашим кодом.

  • 1
    Ваша первая функция может быть использована без использования метода «execute», если она написана следующим образом: {foreach (var e in source) {action(e);} return source;}
  • 3
    Не могли бы вы просто использовать Select вместо метода Apply? Мне кажется, что применение действия к каждому элементу и его получение - это именно то, что делает Select. Например, numbers.Select(n => n*2);
Показать ещё 4 комментария
27

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

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

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

  • 4
    Смотрите также blogs.msdn.com/b/ericlippert/archive/2009/05/18/… . Ddoing нарушает принципы функционального программирования, на которых основаны все остальные операторы последовательности. Очевидно, что единственная цель вызова этого метода - вызвать побочные эффекты.
  • 2
    Это рассуждение полностью поддельное. Вариант использования заключается в том, что необходимы побочные эффекты ... без ForEach вы должны написать цикл foreach. Существование ForEach не стимулирует побочные эффекты, а его отсутствие не уменьшает их.
16

Хотя я согласен с тем, что в большинстве случаев лучше использовать встроенную конструкцию 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
  • 0
    Это отличный метод расширения. Я буду использовать его в будущем.
  • 0
    Отличное расширение, но не могли бы вы добавить документацию? Каково предполагаемое использование возвращаемого значения? Почему index var, а не int? Пример использования?
Показать ещё 6 комментариев
14

Одним из способов решения проблемы является запись .ToList().ForEach(x => ...).

профи

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

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

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

против

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

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

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

  • 0
    а) Это даже не отдаленный ответ на вопрос. б) Этот ответ был бы более понятным, если бы он упомянул List<T>.ForEach что, хотя нет метода 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); }
11

Я всегда удивлялся, что сам, поэтому, что я всегда несли это со мной:

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 может быть нулевым ... вы также можете это проверить.
  • 0
    Да, я проверяю свою библиотеку, я просто пропустил это, когда делал это быстро там.
Показать ещё 3 комментария
7

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

Методы расширения LINQ все возвращают значение, чтобы они могли быть соединены вместе:

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

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

Конкретный аргумент в отношении ForEach заключается в том, что на основе ограничений на методы расширения (а именно, что метод расширения никогда переопределяет унаследованный метод с той же сигнатурой), может возникнуть ситуация, когда пользовательский метод расширения доступен для всех классов, которые impelement IEnumerable <T > , за исключением List <T > . Это может вызвать путаницу, когда методы начинают вести себя по-разному в зависимости от того, вызывается ли метод расширения или метод наследования.

6

Вы можете использовать (привязанный, но лениво оцениваемый) 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, но ленив и цепляется.

  • 0
    Фантастически, никогда не щелкало, что возвращенный выбор будет работать так. Хорошее использование синтаксиса метода, так как я не думаю, что вы могли бы сделать это с синтаксисом выражения (следовательно, я не думал об этом раньше).
  • 0
    @AlexKeySmith Вы могли бы сделать это с синтаксисом выражения, если бы в C # был оператор последовательности, такой как его предшественник. Но весь смысл ForEach в том, что он выполняет действие с типом void, и вы не можете использовать типы void в выражениях в C #.
4

@Coincoin

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

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

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

и т.д...

Логика находится в одном месте, и вы не загрязнили свой класс.

  • 0
    foreach (var p в List1.Concat (List2) .Concat (List3)) {... do stuff ...}
  • 0
    Если это так же просто, как f(p) , то пропустите { } для сокращения: for (var p in List1.Concat(List2).Concat(List2)) f(p); ,
3

Обратите внимание, что MoreLINQ NuGet предоставляет метод расширения ForEach, который вы ищете (а также метод Pipe, который выполняет делегат и дает результат). См:

3

В 3.5 все методы расширения, добавленные в IEnumerable, существуют для поддержки LINQ (обратите внимание, что они определены в классе System.Linq.Enumerable). В этой статье я объясню, почему foreach не принадлежит LINQ: Существующий метод расширения LINQ аналогичен Parallel.For?

2

Чтобы использовать Foreach, ваш список должен быть загружен в первичную память. Но из-за ленивой загрузки IEnumerable они не обеспечивают ForEach в IEnumerable.

Однако при высокой загрузке (используя ToList()) вы можете загрузить свой список в память и воспользоваться преимуществами ForEach.

  • 0
    Эту лень можно наблюдать во время выполнения. Например, если var subList = list.Where(lambda) , то переход на foreach (var e in subList) приведет к foreach (var e in subList) к лямбда-выражению Where's . Обратите внимание, что subList является System.Linq.Enumerable.WhereListIterator .
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

2

Вы можете использовать select, когда хотите что-то вернуть. Если вы этого не сделаете, вы можете использовать ToList сначала, потому что вы, вероятно, не хотите изменять что-либо в коллекции.

  • 0
    а) Это не ответ на вопрос. б) ToList требует дополнительного времени и памяти.
2

Является ли это мной или является List <T> .Foreach в значительной степени устарел 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 и List. Foreach всегда считал меня реализацией шаблона Vistor (см. "Образцы дизайна" от Gamma, Helm, Johnson, Vlissides).

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

  • 1
    Я считаю, что сгенерированный код для foreach должен иметь блок try-finally. Потому что IEnumerator T наследуется от интерфейса IDisposable. Итак, в сгенерированном блоке finally, IEnumerator экземпляра T должен быть утилизирован.
2

Если у вас есть F # (который будет в следующей версии .NET), вы можете использовать

Seq.iter doSomething myIEnumerable

  • 5
    Поэтому Microsoft решила не предоставлять метод ForEach для IEnumerable в C # и VB, потому что он не будет чисто функциональным; тем не менее, они ДАЛИ (назовите его) на более функциональном языке, F #. Их логика намекает на меня.
2

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

  • 2
    ForEach использует Action <T>, который является другой формой делегата
  • 1
    За исключением права Леппи, все методы расширения Enumerable возвращают значение, поэтому ForEach не соответствует шаблону. Делегаты не входят в это.
Показать ещё 18 комментариев
1

Я хотел бы расширить ответ Aku.

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

private static IEnumerable<T> ForEach<T>(IEnumerable<T> xs, Action<T> f) {
    foreach (var x in xs) {
        f(x); yield return x;
    }
}
  • 0
    Это то же самое, что Select(x => { f(x); return x;})
1

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

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

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню