List<T>.ForEach与自定义IEnumerable<T>扩展的比较

4

假设我有一个类:

public class MyClass
{
   ...
}

有一个Web服务方法,返回一个IEnumerable<MyClass>

Web服务的使用者定义了一些方法:

public void DoSomething(MyClass myClass)
{
   ...
}

现在,消费者可以通过两种方式调用webservice方法的结果上的DoSomething函数:
var result = // web service call

foreach(var myClass in result)
{
   DoSomething(myClass);
}

或者:

var result = // web service call

result.ToList().ForEach(DoSomething);

毋庸置疑,我更喜欢第二种方式,因为它更加简洁和表达(一旦你习惯了语法,我已经习惯了)。
现在,Web服务方法仅公开一个 IEnumerable<MyClass>,但实际上返回的是一个 List<MyClass>,这意味着实际序列化对象仍然是一个 List<T>(据我所知)。但是,我发现(使用反射器)Linq 方法 ToList() 复制了 IEnumerable<T> 中所有对象,而不考虑实际运行时类型(在我看来,如果它已经是一个列表,则可以将参数强制转换为 List<T>)。
这显然会带来一些性能开销,特别是对于大型列表(或大型对象列表)。
那么,我该怎么解决这个问题呢?为什么 Linq 没有 ForEach 方法?
顺便说一下,他的问题与 this one 稍有关联。

ToList()方法在对象为值类型时不会创建副本。相反,它创建一个具有相同引用的列表,这样在循环仍然活动时可以修改原始列表(在某些情况下可能需要这样做)。它还允许使用变量委托,以防DoSomething操作依赖于其他内容或是一个参数,这可能是ForEach方法有意义的情况 - 尽管你也可以不用它。 - RedGlyph
6个回答

5

你可以编写扩展方法,但是IEnumerable<T>没有实现ForEach有很多好的理由。第二个示例:

result.ToList().ForEach(DoSomething);

将IEnumerable复制到List中(除非它已经是List,我假设),因此最好使用传统的foreach(var r in result) {}遍历IEnumerable。

补充:

对我来说,Eric Lippert文章的关键点在于添加ForEach没有任何好处,并且会带来一些潜在的问题:

第二个原因是这样做并没有为语言增加任何新的表现力。这样做让您可以将这个完全清晰的代码:

foreach(Foo foo in foos){ statement involving foo; }

转换成这个代码:

foos.ForEach((Foo foo)=>{ statement involving foo; });

几乎使用相同的字符但顺序略有不同。然而,第二个版本更难理解、更难调试,并引入了闭包语义,从而可能以微妙的方式改变对象生命周期。


1
+1 链接到 Eric Lippert 的权威文章,解释为什么在 Linq 中没有 ForEach。 - Gabe Moothart
这是正确的,LINQ 是为查询而设计的,而不是迭代。 - sidney.andrews
我同意Eric对他的例子的分析,但我认为他忽略了这一点:foos.ForEach(SomeMethod)。没有需要讨论的闭包语义,而且在我看来,当你只想为列表中的每个项目调用一个方法时,它实际上比foreach循环更容易阅读和更简洁。由于明显的好处和缺乏重大缺点,我继续实现了自己的扩展方法,并且我永远不会放弃它! - Joel Mueller

5

I would prefer this:-

foreach (var item in result.ToList())
{
   DoSomething(item);
}

这是一个更清晰的惯用语,它表示将一系列东西收集在一起然后执行可能改变应用程序状态的重要操作。虽然有点老派,但它确实更容易被广大人群理解。


我同意使用foreach,但如果已经是IEnumerable,为什么要转换成List? - Jamie Ide
@Jamie:ToList是因为它在原始问题中。有时候这是必要的。例如,如果DoSomething代表从IEnumerable的最终源中删除项目的操作,那么如果不包括ToList,你很可能会得到异常。当然,如果你确定DoSomething不会这样做,那么很可能就不需要了。 - AnthonyWJones
您还可以在 ForEach 委托参数上设置断点,以便在任何情况下都能轻松进行调试。 - RedGlyph
@RedGlyph:好观点,我已经删除了Brian对我的回答的编辑。对于有人在我的回答中添加那样的内容,我稍微有点生气,这正常吗? - AnthonyWJones

4

我使用两种方法。一种是遍历列表,另一种是使用惰性求值。我根据情况选择使用它们。

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

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

2
您可以像这样为 IEnumerable<T> 编写自己的扩展方法:
    public static void ForEach<T>(this IEnumerable<T> enumerable, Action<T> action)
    {
        foreach (T t in enumerable)
            action(t);
    } 

Linq中没有这样的方法,因为Linq主要用于查询,而不是简单的迭代。

此外,请注意,当使用实际的List<T>实例时,扩展方法将不会被调用,因为当它们共享签名时,实例方法具有优先权。

例如,以下代码将不会调用扩展方法:

 var l = new List<MyClass>();
 l.Add(new MyClass());
 l.ForEach(DoSomething);

以下内容会这样表述:

而以下内容则会:

IEnumerable<MyClass> l = new List<MyClass>(new []{new MyClass()});
l.ForEach(DoSomething);  

2
说实话,我真的不喜欢这个。它没有错,所以我不会给它点踩,但你在这里混淆了函数式(闭包)和命令式语义,这很容易导致意外、难以调试的问题,或者最多只能得到很差的可读性。 - Aaronaught

1

你可以编写自己的扩展方法ToList(this List theList){return theList;},从而避免开销。由于你的扩展方法是最具体的,它将被调用,而不是IEnumerable上的方法。


-2
如果您决定通过扩展方法来实现这一点,我建议将其命名为ForAll而不是ForEach。这样可以使用与.NET 4.0中的Parallel Extensions相同的语法:
var result = // web service call
result.AsParallel().ForAll(DoSomething);

为什么要称其为ForAll,这会与并行化版本产生混淆,而已经存在一个具有完全相同语义(只是不同类型)的现有框架方法ForEach - Pavel Minaev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接