LINQ еквівалент foreach для IEnumerable <T>


717

Я хотів би зробити еквівалент наступного в LINQ, але не можу зрозуміти, як:

IEnumerable<Item> items = GetItems();
items.ForEach(i => i.DoStuff());

Що таке справжній синтаксис?


99
Розгляньте "foreach" vs. "ForEach" Еріка Ліпперта. Це забезпечує гарне виправдання НЕ використовуючи / подачі ForEach().

4
@pst: Цей метод розширення я сліпо дотримувався у своїх проектах. Дякую за цю статтю
Робін Мабен


2
Хоча це не дуже сексуально, це вписується в один рядок, не створюючи копію через ToList -foreach (var i in items) i.Dostuff();
Джеймс Вестгейт,

1
Деякі з цих посилань мертві! Що за стаття, за яку так багато голосували!
Воррен

Відповіді:


863

Немає розширення ForEach для IEnumerable; тільки для List<T>. Так ви могли зробити

items.ToList().ForEach(i => i.DoStuff());

Крім того, напишіть власний метод розширення ForEach:

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

213
Будьте обережні з ToList (), оскільки ToList () створює копію послідовності, яка може спричинити проблеми з продуктивністю та пам'яттю.
decasteljau

15
Є кілька причин, чому ".Foreach ()" стане кращим. 1. Мультиварка, можлива PLINQ. 2. Винятки, можливо, ви захочете зібрати всі винятки у циклі та одразу перекинути його; і вони є шумовими кодами.
Dennis C

12
PLINQ використовує ForAll не ForEach, тому ви не використовуєте ForEach.
user7116

9
Вам слід повернути метод IENumerable<T>у розширенні. Як:public static IEnumerable<T> ForAll<T>(this IEnumerable<T> numeration, Action<T> action) { foreach (T item in enumeration) { action(item); } return enumeration; }
Kees C. Bakker

22
Зауважте, що в першому випадку розробник 1) майже не зберігає введення тексту та 2) необґрунтовано виділяє пам'ять, щоб зберігати всю послідовність як список, перш ніж викидати її. Подумайте, перш ніж ви LINQ
Марк Совул

364

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

Це не означає, що це погано - просто думати про філософські причини, що стоять за рішенням.


44
І все-таки F # має Seq.iter
Стівен Свенсен

6
Однак є одна дуже корисна річ, що функції LINQ зазвичай передбачають, що foreach () не робить - анонімна функція, прийнята функціями Linq, також може приймати індекс як параметр, тоді як з foreach ви повинні відновити класичний для (int i .. ) побудувати. І функціональні мови повинні дозволити ітерацію, яка має побічні ефекти - інакше ви ніколи не зможете зробити з ними жодну йоту роботи :) Я хотів би зазначити, що типовим функціональним методом було б повернення оригіналу без змін.
Walt W

3
@Walt: Я все ще не впевнений, що я згоден, почасти тому, що типовий по-справжньому функціональний підхід не використовує подібне передбачення ... він би використовував функціональний підхід, як LINQ, створюючи нову послідовність.
Джон Скіт

12
@Stephen Swensen: так, F # має Seq.iter, але F # також має незмінні структури даних. при використанні Seq.iter у цих випадках вихідний Seq не змінюється; повертається новий Seq з побічними елементами.
Андерс

7
@Anders - Seq.iterнічого не повертає, не кажучи вже про нову послідовність із побічними елементами. Дійсно йдеться лише про побічні ефекти. Ви можете подумати Seq.map, адже Seq.iterце точно рівнозначно ForEachметоду розширення на IEnumerabe<_>.
Джоель Мюллер

39

Оновлення 17.07.2012: Мабуть, станом на C # 5.0, поведінка foreachописаної нижче змінилася, і " використання foreachзмінної ітерації в вкладеному лямбда-виразі більше не дає несподіваних результатів ". Ця відповідь не стосується C # ≥ 5.0 .

@John Skeet і всі, хто віддає перевагу ключовому слову foreach.

Проблема "foreach" в C # до 5,0 , полягає в тому, що вона не відповідає тому, як працює еквівалент "для розуміння" іншими мовами, і як я б очікував, що це буде працювати (особиста думка, викладена тут лише тому, що інші згадували про своє думка щодо читабельності). Перегляньте всі питання, що стосуються " Доступ до модифікованого закриття ", а також " Закриття змінної циклу, що вважається шкідливим ". Це лише "шкідливо" через те, як "foreach" реалізований у C #.

Візьміть наступні приклади, використовуючи функціонально еквівалентний метод розширення, який відповідає у відповіді @Fredrik Kalseth.

public static class Enumerables
{
    public static void ForEach<T>(this IEnumerable<T> @this, Action<T> action)
    {
        foreach (T item in @this)
        {
            action(item);
        }
    }
}

Вибачення за надмірно надуманий приклад. Я використовую лише спостережуване, тому що робити щось подібне не зовсім далеко. Очевидно, що є кращі способи створити це спостережуване, я лише намагаюся продемонструвати точку. Зазвичай код, підписаний на спостережуване, виконується асинхронно і потенційно в іншій потоці. Якщо використовувати "foreach", це може призвести до дуже дивних та потенційно недетермінованих результатів.

Наступний тест з використанням методу розширення "ForEach" проходить:

[Test]
public void ForEachExtensionWin()
{
    //Yes, I know there is an Observable.Range.
    var values = Enumerable.Range(0, 10);

    var observable = Observable.Create<Func<int>>(source =>
                            {
                                values.ForEach(value => 
                                    source.OnNext(() => value));

                                source.OnCompleted();
                                return () => { };
                            });

    //Simulate subscribing and evaluating Funcs
    var evaluatedObservable = observable.ToEnumerable().Select(func => func()).ToList();

    //Win
    Assert.That(evaluatedObservable, 
        Is.EquivalentTo(values.ToList()));
}

Наступні помилки з помилкою:

Очікувано: еквівалентно <0, 1, 2, 3, 4, 5, 6, 7, 8, 9> Але було: <9, 9, 9, 9, 9, 9, 9, 9, 9, 9>

[Test]
public void ForEachKeywordFail()
{
    //Yes, I know there is an Observable.Range.
    var values = Enumerable.Range(0, 10);

    var observable = Observable.Create<Func<int>>(source =>
                            {
                                foreach (var value in values)
                                {
                                    //If you have resharper, notice the warning
                                    source.OnNext(() => value);
                                }
                                source.OnCompleted();
                                return () => { };
                            });

    //Simulate subscribing and evaluating Funcs
    var evaluatedObservable = observable.ToEnumerable().Select(func => func()).ToList();

    //Fail
    Assert.That(evaluatedObservable, 
        Is.EquivalentTo(values.ToList()));
}

Що говорить попередження про повторне формування?
reggaeguitar

1
@reggaeguitar Я не використовував C # протягом 7 або 8 років, оскільки до виходу C # 5.0 було випущено, що змінило описану тут поведінку. На той момент це повідомлення було попереджено stackoverflow.com/questions/1688465/… . Я сумніваюся, що це все ще попереджає про це.
drstevens

34

Ви можете використовувати FirstOrDefault()розширення, яке доступне для IEnumerable<T>. Повернувшись falseіз присудка, він буде запущений для кожного елемента, але не буде байдужим, що він насправді не знайде відповідності. Це дозволить уникнути ToList()накладних витрат.

IEnumerable<Item> items = GetItems();
items.FirstOrDefault(i => { i.DoStuff(); return false; });

11
Я теж згоден. Це може бути хитрий хакер, але на перший погляд, цей код не зрозуміло, що він робить, звичайно, порівняно зі стандартним циклом foreach.
Коннелл

26
Мені довелося брати участь у програмі, оскільки, будучи технічно правильним, ваше майбутнє «я» та всі ваші наступники назавжди будуть вас ненавидіти, тому що замість foreach(Item i in GetItems()) { i.DoStuff();}вас взяли більше персонажів і зробили це надзвичайно заплутаним
Марк Соул

14
Гаразд, але прочитайте більш читабельний хак:items.All(i => { i.DoStuff(); return true; }
nawfal

5
Я знаю, що це досить стара посада, але мені довелося також її зневажати. Я погоджуюсь, що це розумний трюк, але це не піддається ремонту. На перший погляд схоже, що щось роблять із першим пунктом у списку. Це може легко призвести до появи більше помилок у майбутньому, якщо кодери нерозуміють, як це функціонує та що він повинен робити.
Лі

4
Це програмування побічних ефектів і відмовляє від IMHO.
Аніш

21

Я взяв метод Фредріка і змінив тип повернення.

Таким чином, метод підтримує відкладене виконання як і інші методи LINQ.

EDIT: Якщо цього не було зрозуміло, будь-яке використання цього методу повинно закінчуватися ToList () або будь-яким іншим способом, щоб змусити метод працювати на цілому переліку. Інакше дія не буде виконана!

public static IEnumerable<T> ForEach<T>(this IEnumerable<T> enumeration, Action<T> action)
{
    foreach (T item in enumeration)
    {
        action(item);
        yield return item;
    }
}

А ось тест, який допоможе побачити це:

[Test]
public void TestDefferedExecutionOfIEnumerableForEach()
{
    IEnumerable<char> enumerable = new[] {'a', 'b', 'c'};

    var sb = new StringBuilder();

    enumerable
        .ForEach(c => sb.Append("1"))
        .ForEach(c => sb.Append("2"))
        .ToList();

    Assert.That(sb.ToString(), Is.EqualTo("121212"));
}

Якщо ви видалите ToList () в кінці кінців, ви побачите тест, який не вдався, оскільки StringBuilder містить порожню рядок. Це тому, що жоден метод не змусив ForEach перерахувати.


Ваша альтернативна реалізація ForEachцікава, проте вона не відповідає поведінці List.ForEach, підпис якої є public void ForEach(Action<T> action). Це також не відповідає поведінці Observable.ForEachрозширення до IObservable<T>, підпис якого є public static void ForEach<TSource>(this IObservable<TSource> source, Action<TSource> onNext). FWIW, ForEachеквівалент колекцій Scala, навіть тих, хто лінивий, також має тип повернення, еквівалентний недійсності в C #.
drstevens

Це найкраща відповідь: відкладене виконання + вільне api.
Олександр Данилов

3
Це працює точно так само , як виклик Selectз ToList. Мета ForEach- не потребувати дзвінків ToList. Це слід виконати негайно.
t3chb0t

3
Яка користь матиме цей "ForEach", виконання якого відкладено, порівняно з тим, що виконується негайно? Відстрочка (що незручно в тому, як використовуватиметься ForEach) не змінює той факт, що ForEach виконує лише корисну роботу, якщо дія має побічні ефекти (звичайна скарга, подана проти такого оператора Linq). Отже, як же це зміна допомоги? Крім того, доданий "ToList ()", щоб змусити його виконувати, не виконує корисних цілей. Це нещасний випадок, який чекає. Сенс "ForEach" - це його побічні ефекти. Якщо Ви хочете повернутий перерахунок, SELECT має більше сенсу.
ToolmakerSteve

20

Тримайте ваші побічні ефекти від моїх безлічі

Я хотів би зробити еквівалент наступного в LINQ, але не можу зрозуміти, як:

Як вказували інші тут і за кордоном LINQ іIEnumerable , очікується, що методи не мають побічних ефектів.

Ви дійсно хочете "зробити щось" для кожного пункту в IEnumerable? Тоді foreachнайкращий вибір. Люди не здивовані, коли тут виникають побічні ефекти.

foreach (var i in items) i.DoStuff();

Б'юсь об заклад, що ви не хочете побічного ефекту

Однак, на мій досвід, побічні ефекти зазвичай не потрібні. Найчастіше існує простий запит LINQ, який очікує на виявлення, а також відповідь StackOverflow.com або Джон Скіт, Ерік Ліпперт, або Марк Гравелл, що пояснює, як робити те, що ви хочете!

Деякі приклади

Якщо ви насправді просто агрегуєте (накопичуєте) якесь значення, то слід розглянути Aggregateметод розширення.

items.Aggregate(initial, (acc, x) => ComputeAccumulatedValue(acc, x));

Можливо, ви хочете створити нове IEnumerableз існуючих значень.

items.Select(x => Transform(x));

Або, можливо, ви хочете створити таблицю перегляду:

items.ToLookup(x, x => GetTheKey(x))

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


7
Так, виберіть "Карта", а "Сукупність" - зменшити.
Даррел Лі

17

Якщо ви хочете виконувати роль перерахунку, ви повинні отримати кожен елемент.

public static class EnumerableExtensions
{
    public static IEnumerable<T> ForEach<T>(this IEnumerable<T> enumeration, Action<T> action)
    {
        foreach (var item in enumeration)
        {
            action(item);
            yield return item;
        }
    }
}

Це насправді не ForEach, коли ви повертаєте товар, це більше схоже на Tap.
EluciusFTW

10

Існує експериментальна версія Microsoft Interactive Extensions до LINQ (також на NuGet , див . Профіль RxTeams для отримання додаткових посилань). Channel 9 відео пояснює це добре.

Документи надаються лише у форматі XML. Я запустив цю документацію в Sandcastle, щоб вона могла бути в більш читаному форматі. Розпакуйте архів документів і знайдіть index.html .

Серед багатьох інших вигод, він забезпечує очікуване впровадження ForEach. Це дозволяє писати такий код:

int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8 };

numbers.ForEach(x => Console.WriteLine(x*x));

2
Робоче посилання на інтерактивні розширення: microsoft.com/download/uk/details.aspx?id=27203
Maciek Świszczowski

8

Відповідно до PLINQ (доступний з .Net 4.0), ви можете зробити це

IEnumerable<T>.AsParallel().ForAll() 

зробити паралельний цикл foreach на IEnumerable.


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

2
Правда. Це не безпечно для потоків, з різних пулів ниток будуть використовуватися різні теми ... І мені потрібно переглянути свою відповідь. Коли я пробую його зараз, немає ForEach (). Має бути мій код, що містить розширення з ForEach, коли я розмірковував над цим.
Вовк5

6

Мета ForEach - викликати побічні ефекти. IEnumerable - це для ледачого перерахування набору.

Ця концептуальна різниця досить помітна, коли ви її враховуєте.

SomeEnumerable.ForEach(item=>DataStore.Synchronize(item));

Це звичайне виконання, поки ви не зробите "count" або "ToList ()" чи щось на ньому. Це явно не те, що виражається.

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


5

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

IEnumerable<Item> items = GetItems();
foreach (var item in items) item.DoStuff();

Короткий і простий (ст).


Ви можете залишити весь перший рядок і використовувати GetItems безпосередньо в foreach
tymtam

3
Звичайно. Це написано так для наочності. Так що зрозуміло, який GetItems()метод повертається.
Ненад

4
Тепер, коли я читаю свій коментар, я бачу, ніби я кажу вам щось, чого ви не знаєте. Я цього не мав на увазі. Я мав на увазі, що foreach (var item in GetItems()) item.DoStuff();це просто краса.
tymtam

4

Тепер у нас є можливість ...

        ParallelOptions parallelOptions = new ParallelOptions();
        parallelOptions.MaxDegreeOfParallelism = 4;
#if DEBUG
        parallelOptions.MaxDegreeOfParallelism = 1;
#endif
        Parallel.ForEach(bookIdList, parallelOptions, bookID => UpdateStockCount(bookID));

Звичайно, це відкриває цілком нову банку ниток.

ps (Вибачте за шрифти, це вирішила система)


4

Як уже зазначаються численні відповіді, ви можете легко додати такий метод розширення самостійно. Однак якщо ви цього не хочете робити, хоча мені в BCL нічого подібного не відомо, у Systemпросторі імен все ще є варіант , якщо ви вже маєте посилання на Реактивне розширення (і якщо ви цього не робите , ти повинен мати):

using System.Reactive.Linq;

items.ToObservable().Subscribe(i => i.DoStuff());

Хоча назви методів трохи відрізняються, кінцевий результат - саме те, що ви шукаєте.


Схоже, цей пакет застарілий
reggaeguitar

3

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


Employees.ForEach(e=>e.Act_A)
         .ForEach(e=>e.Act_B)
         .ForEach(e=>e.Act_C);

Orders  //just for demo
    .ForEach(o=> o.EmailBuyer() )
    .ForEach(o=> o.ProcessBilling() )
    .ForEach(o=> o.ProcessShipping());


//conditional
Employees
    .ForEach(e=> {  if(e.Salary<1000) e.Raise(0.10);})
    .ForEach(e=> {  if(e.Age   >70  ) e.Retire();});

Нетерплячий версія реалізації.

public static IEnumerable<T> ForEach<T>(this IEnumerable<T> enu, Action<T> action)
{
    foreach (T item in enu) action(item);
    return enu; // make action Chainable/Fluent
}

Edit: Ледача версія використовує повернення прибутковості, як це .

public static IEnumerable<T> ForEachLazy<T>(this IEnumerable<T> enu, Action<T> action)
{
    foreach (var item in enu)
    {
        action(item);
        yield return item;
    }
}

Лідні версії НЕ потребують матеріалізації, наприклад, ToList (), інакше нічого не відбувається. дивіться нижче чудові коментарі від ToolmakerSteve.

IQueryable<Product> query = Products.Where(...);
query.ForEachLazy(t => t.Price = t.Price + 1.00)
    .ToList(); //without this line, below SubmitChanges() does nothing.
SubmitChanges();

Я зберігаю і ForEach (), і ForEachLazy () у своїй бібліотеці.


2
Ви перевірили це? Існує декілька контекстів, коли цей метод поводиться протипоказано інтуїтивно. Краще будеforeach(T item in enu) {action(item); yield return item;}
Taemyr

2
Це призведе до кількох перерахувань
regisbsb

Цікаво. Обмірковуючи, коли я хотів би зробити вище, для послідовності з 3 дій, а не foreach (T item in enu) { action1(item); action2(item); action3(item); }? Єдиний, явний цикл. Я здогадуюсь, якби важливо було виконати всі дії1, перш ніж почати робити action2.
ToolmakerSteve

@ Rm558 - ваш підхід із використанням "прибутку". Pro: перераховує оригінальну послідовність лише один раз, тому правильно працює з ширшим діапазоном перерахувань. Con: якщо ви забудете ".ToArray ()" наприкінці, нічого не станеться. Хоча невдача буде очевидною, її не помічають надовго, тож не великі витрати. Для мене це не "чисте", але це правильний компроміс, якщо хтось йде по цій дорозі (оператор ForEach).
ToolmakerSteve

1
Цей код не є безпечним з транзакційної точки зору. Що робити, якщо вона не вдається ProcessShipping()? Ви посилаєте покупця електронною поштою, стягуєте його кредитну картку, але ніколи не надсилаєте йому свої речі? Безумовно, просять проблем.
zmechanic

2

Цей "функціональний підхід" абстракція просочується великим часом. Ніщо на мовному рівні не запобігає побічним ефектам. Поки ви зможете викликати його лямбда / делегата для кожного елемента контейнера - ви отримаєте поведінку "ForEach".

Ось, наприклад, один із способів об’єднання srcDictionary в destDictionary (якщо ключ вже існує - перезаписується)

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

var b = srcDictionary.Select(
                             x=>
                                {
                                  destDictionary[x.Key] = x.Value;
                                  return true;
                                }
                             ).Count();

6
Якщо вам доведеться додати цю відмову, ви, ймовірно, не повинні її публікувати. Просто моя думка.
Ендрю Гроте

2
Як чорт це кращий foreach(var tuple in srcDictionary) { destDictionary[tuple.Key] = tuple.Value; }? Якщо немає у виробничому коді, де і навіщо вам це використовувати?
Марк Совул

3
Це просто показати це можливо в принципі. Також трапляється робити те, про що просив ОП, і я чітко зазначив, що його не слід використовувати у виробництві, все ще цікаво і має навчальну цінність.
Зар Шардан

2

Натхненний Джоном Скітом, я розширив його рішення наступним чином:

Спосіб розширення:

public static void Execute<TSource, TKey>(this IEnumerable<TSource> source, Action<TKey> applyBehavior, Func<TSource, TKey> keySelector)
{
    foreach (var item in source)
    {
        var target = keySelector(item);
        applyBehavior(target);
    }
}

Клієнт:

var jobs = new List<Job>() 
    { 
        new Job { Id = "XAML Developer" }, 
        new Job { Id = "Assassin" }, 
        new Job { Id = "Narco Trafficker" }
    };

jobs.Execute(ApplyFilter, j => j.Id);

. . .

    public void ApplyFilter(string filterId)
    {
        Debug.WriteLine(filterId);
    }


1

Я з повагою не погоджуюся з думкою, що методи розширення посилань повинні бути без побічних ефектів (не тільки тому, що вони відсутні, будь-який делегат може виконувати побічні ефекти).

Розглянемо наступне:

   public class Element {}

   public Enum ProcessType
   {
      This = 0, That = 1, SomethingElse = 2
   }

   public class Class1
   {
      private Dictionary<ProcessType, Action<Element>> actions = 
         new Dictionary<ProcessType,Action<Element>>();

      public Class1()
      {
         actions.Add( ProcessType.This, DoThis );
         actions.Add( ProcessType.That, DoThat );
         actions.Add( ProcessType.SomethingElse, DoSomethingElse );
      }

      // Element actions:

      // This example defines 3 distict actions
      // that can be applied to individual elements,
      // But for the sake of the argument, make
      // no assumption about how many distict
      // actions there may, and that there could
      // possibly be many more.

      public void DoThis( Element element )
      {
         // Do something to element
      }

      public void DoThat( Element element )
      {
         // Do something to element
      }

      public void DoSomethingElse( Element element )
      {
         // Do something to element
      }

      public void Apply( ProcessType processType, IEnumerable<Element> elements )
      {
         Action<Element> action = null;
         if( ! actions.TryGetValue( processType, out action ) )
            throw new ArgumentException("processType");
         foreach( element in elements ) 
            action(element);
      }
   }

Що показує приклад, насправді є лише різновидом пізньої прив'язки, який дозволяє викликати одну з багатьох можливих дій, що мають побічні ефекти на послідовність елементів, без необхідності писати велику конструкцію комутатора для декодування значення, що визначає дію та перекладати це у відповідний метод.


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

1

Щоб залишатися вільним, можна скористатися такою хитрістю:

GetItems()
    .Select(i => new Action(i.DoStuf)))
    .Aggregate((a, b) => a + b)
    .Invoke();


-1

Ще один ForEachприклад

public static IList<AddressEntry> MapToDomain(IList<AddressModel> addresses)
{
    var workingAddresses = new List<AddressEntry>();

    addresses.Select(a => a).ToList().ForEach(a => workingAddresses.Add(AddressModelMapper.MapToDomain(a)));

    return workingAddresses;
}

4
Яка трата пам’яті. Це закликає ToList додати до списку. Чому це не просто повертає адреси. Виберіть (a => MapToDomain (a))?
Марк Совул
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.