我想在LINQ中执行以下等效操作,但我无法确定如何执行:

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

真正的语法是什么?


当前回答

这么多的答案,但所有人都未能找到自定义通用ForEach扩展的一个非常重要的问题:性能!更具体地说,内存使用和GC。

考虑以下示例。针对.NET Framework 4.7.2或.NET Core 3.1.401,配置为Release,平台为Any CPU。

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

class Program
{
    private static void NoOp(int value) {}

    static void Main(string[] args)
    {
        var list = Enumerable.Range(0, 10).ToList();
        for (int i = 0; i < 1000000; i++)
        {
            // WithLinq(list);
            // WithoutLinqNoGood(list);
            WithoutLinq(list);
        }
    }

    private static void WithoutLinq(List<int> list)
    {
        foreach (var item in list)
        {
            NoOp(item);
        }
    }

    private static void WithLinq(IEnumerable<int> list) => list.ForEach(NoOp);

    private static void WithoutLinqNoGood(IEnumerable<int> enumerable)
    {
        foreach (var item in enumerable)
        {
            NoOp(item);
        }
    }
}

乍一看,这三种变体都应该表现得同样出色。然而,当ForEach扩展方法被多次调用时,最终会产生垃圾,这意味着代价高昂的GC。事实上,在热路径上使用此ForEach扩展方法已被证明会完全破坏循环密集型应用程序的性能。

类似地,弱类型的foreach循环也会产生垃圾,但它仍然比foreach扩展更快,占用的内存更少(它也会受到委托分配的影响)。

强类型foreach:内存使用

弱类型foreach:内存使用

ForEach扩展:内存使用

分析

对于强类型foreach,编译器可以使用类的任何优化枚举器(例如基于值的),而泛型foreach扩展必须返回到将在每次运行时分配的泛型枚举器。此外,实际代表还将意味着额外分配。

使用WithoutLinqNoGood方法也会得到类似的坏结果。在那里,参数的类型是IEnumerable<int>,而不是List<int>。这意味着相同类型的枚举器分配。

以下是IL中的相关差异。基于值的枚举器当然更可取!

IL_0001:  callvirt   instance class
          [mscorlib]System.Collections.Generic.IEnumerator`1<!0> 
          class [mscorlib]System.Collections.Generic.IEnumerable`1<!!T>::GetEnumerator()

vs

IL_0001:  callvirt   instance valuetype
          [mscorlib]System.Collections.Generic.List`1/Enumerator<!0>
          class [mscorlib]System.Collections.Generic.List`1<int32>::GetEnumerator()

结论

OP询问如何在IEnumerable<T>上调用ForEach()。最初的答案清楚地表明了如何做到这一点。当然你能做到,但再说一遍;我的回答清楚地表明你不应该。

在针对.NET Core 3.1.401(使用Visual Studio 16.7.2编译)时验证了相同的行为。

其他回答

又一个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;
}

正如许多答案已经指出的那样,您可以自己轻松地添加这样的扩展方法。然而,如果您不想这样做,尽管我不知道BCL中有什么类似的内容,但如果您已经引用了Reactive Extension(如果没有,您应该有),System命名空间中仍然有一个选项:

using System.Reactive.Linq;

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

虽然方法名称有点不同,但最终结果正是您所希望的。

IEnumerable没有ForEach扩展;仅适用于列表<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);
    }
}

微软发布了一个LINQ交互扩展的实验版本(也在NuGet上,更多链接请参见RxTeams的简介)。9频道的视频很好地解释了这一点。

它的文档仅以XML格式提供。我在Sandcastle中运行了这个文档,以使其具有更可读的格式。解压缩文档存档并查找index.html。

在许多其他好处中,它提供了预期的ForEach实现。它允许您编写如下代码:

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

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

2012年7月17日更新:显然,从C#5.0开始,下面描述的foreach的行为已经改变,“在嵌套lambda表达式中使用foreach迭代变量不再产生意外结果。”这个答案不适用于C#≥5.0。

@约翰·斯基特和所有喜欢foreach关键字的人。

5.0之前的C#中的“foreach”的问题是,它与其他语言中等效的“for understanding”的工作方式不一致,也与我期望它的工作方式(此处陈述的个人观点仅是因为其他人提到了他们对可读性的看法)不一致。查看有关“修改关闭的访问”的所有问题以及“关闭被认为有害的循环变量”。这只是“有害的”,因为“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);
        }
    }
}

为这个过于做作的例子道歉。我只使用Observable,因为这样做并不完全牵强。显然,有更好的方法来创建这种可观察性,我只是试图证明一点。通常,订阅可观察到的代码是异步执行的,并且可能在另一个线程中执行。如果使用“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

[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()));
}