【问题标题】:Executing a certain action for all elements in an Enumerable<T>对 Enumerable<T> 中的所有元素执行特定操作
【发布时间】:2010-10-06 11:07:47
【问题描述】:

我有一个Enumerable&lt;T&gt;,我正在寻找一种方法,可以让我对每个元素执行一个操作,有点像Select,但会产生副作用。比如:

string[] Names = ...;
Names.each(s => Console.Writeline(s));

Names.each(s => GenHTMLOutput(s));   
// (where GenHTMLOutput cannot for some reason receive the enumerable itself as a parameter)

我确实尝试了Select(s=&gt; { Console.WriteLine(s); return s; }),但它没有打印任何内容。

【问题讨论】:

标签: linq c#-3.0 enumerable


【解决方案1】:

免责声明:这篇文章不再像我原来的答案,而是结合了我从那以后获得的大约七年的经验。我进行了编辑,因为这是一个备受关注的问题,并且现有的答案都没有真正涵盖所有角度。如果您想查看我的原始答案,可以在revision history for this post 中找到。


这里首先要了解的是 C# linq 操作,如 Select()All()Where() 等,其根源在于 functional programming。这个想法是将函数式编程的一些更有用和更容易理解的部分带到 .Net 世界。这很重要,因为函数式编程的一个关键原则是操作没有副作用。很难低估这一点。但是,在ForEach()/each() 的情况下,副作用是操作的全部目的。添加each()ForEach() 不仅超出了其他linq 运算符的函数式编程范围,而且与它们直接相反。

但我知道这让人不满意。这可能有助于解释为什么框架中省略了ForEach(),但未能解决手头的真正问题。你有一个真正的问题需要解决。为什么所有这些象牙塔哲学都会妨碍一些可能真正有用的东西?

当时在 C# 设计团队工作的 Eric Lippert 可以在这里为我们提供帮助。 He recommends using a traditional foreach loop:

[ForEach()] 为语言添加了零新的表示能力。这样做可以让你重写这段非常清晰的代码:

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

进入这段代码:

foos.ForEach(foo=&gt;{ statement involving foo; });

他的观点是,当您仔细查看语法选项时,您不会从 ForEach() 扩展与传统的 foreach 循环中获得任何新的东西。我部分不同意。想象一下你有这个:

 foreach(var item in Some.Long(and => possibly)
                         .Complicated(set => ofLINQ)
                         .Expression(to => evaluate))
 {
     // now do something
 } 

此代码混淆了含义,因为它将foreach 关键字与循环中的操作分开。它还列出了定义循环操作顺序的操作的循环命令之前。想要先执行这些操作,然后在查询定义的end 处添加循环命令,感觉要自然得多。此外,代码很丑陋。好像能写这个会好很多:

Some.Long(and => possibly)
   .Complicated(set => ofLINQ)
   .Expression(to => evaluate)
   .ForEach(item => 
{
    // now do something
});

然而,即使在这里,我最终还是接受了 Eric 的观点。我意识到你在上面看到的代码正在调用一个额外的变量。如果您有一组复杂的 LINQ 表达式,您可以通过首先将 LINQ 表达式的结果分配给新变量来向代码中添加有价值的信息:

var queryForSomeThing = Some.Long(and => possibly)
                        .Complicated(set => ofLINQ)
                        .Expressions(to => evaluate);
foreach(var item in queryForSomeThing)
{
    // now do something
}

这段代码感觉更自然。它将foreach 关键字放回循环的其余部分旁边,并在查询定义之后。最重要的是,变量名称可以添加新信息,这将有助于未来的程序员试图理解 LINQ 查询的目的。同样,我们看到所需的 ForEach() 运算符确实没有为语言增加新的表达能力。

但是,我们仍然缺少假设的 ForEach() 扩展方法的两个特征:

  1. 它不可组合。在与其余代码内联的foreach 循环之后,我无法再添加.Where()GroupBy()OrderBy(),除非创建新语句。
  2. 这不是懒惰。这些操作立即发生。例如,它不允许我有一个表单,其中用户选择一个操作作为大屏幕中的一个字段,直到用户按下命令按钮才起作用。这种形式可能允许用户在执行命令之前改变主意。这对于 LINQ 查询来说是完全正常的(简单甚至),但对于 foreach 则不那么简单。

(FWIW,大多数幼稚的 .ForEach() 实现也存在这些问题。但没有它们也可以制作一个。)

当然,您可以创建自己的 ForEach() 扩展方法。其他几个答案已经实现了这种方法;这并不是那么复杂。不过,我觉得没必要。从语义和操作的角度来看,已经有一种适合我们想要做的现有方法。上述两个缺失的功能都可以通过使用现有的Select() 操作来解决。

Select() 符合上述两个示例所描述的转换或投影类型。但请记住,我仍然会避免产生副作用。对Select() 的调用应返回新对象或原始对象的投影。这有时可以通过使用匿名类型或动态对象来帮助(如果且​​仅在必要时)。如果您需要将结果保存在原始列表变量中,您可以随时调用.ToList() 并将其分配回原始变量。我将在这里补充一点,我更喜欢使用IEnumerable&lt;T&gt; 变量,而不是更具体的类型。

myList = myList.Select(item => new SomeType(item.value1, item.value2 *4)).ToList();

总结:

  1. 大部分时间都坚持使用foreach
  2. 如果foreach 真的 不行(这可能不像您想象的那么频繁),请使用Select()
  3. 当您需要使用Select() 时,通常仍然可以避免(程序可见的)副作用,可能通过投影到匿名类型。
  4. 避免打电话给ToList()。您并不像您想象的那样需要它,它可能会对性能和内存使用产生重大负面影响。

【讨论】:

  • Downvoted:如果谓词继续返回“true”,这将只处理所有项目。如果您正在执行一个返回布尔值的方法,很容易在没有意识到的情况下意外引入错误。
  • @PeterMorris 我相信他的意思是你可以使用 lambda 来做任何你想做的事情,当你完成执行代码时,只需返回 true。在这种情况下,您可能希望使用需要花括号的语句 lambda。如果您只想使用内置功能并保留惰性求值,这不是一个糟糕的解决方案。
  • @AlexJorgenson .All() 应该是没有副作用的,这不是处理这种事情的好方法,因为它的目的是为了其他目的
  • @Peter Morris 绝对是。我在完全接受之前写了这篇文章。我了解 linq 运算符,但不太了解它们背​​后的函数式编程根源。今天,我同意Eric Lippert's recommendation to just use foreach.
  • 哦... foreach 的另一个抱怨是它不返回序列。但是,如果您需要,我会查看返回新对象(无副作用)而不是 All()Select()。七年很长;)
【解决方案2】:

您正在寻找目前仅存在于 List 泛型集合中的难以捉摸的 ForEach。网上有很多关于微软是否应该将其添加为 LINQ 方法的讨论。目前,您必须自己动手:

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

虽然All() 方法提供了类似的功能,但它的用例是对每个项目而不是操作执行谓词测试。当然,可以说服它执行其他任务,但这会在一定程度上改变语义,并使其他人更难解释您的代码(即,All() 是用于谓词测试还是动作?)。

【讨论】:

  • 如果可以添加,我会更喜欢它。包括采用 Func 操作并返回 IEnumerable 的重载。即使已经可以使用 Select 完成。
  • 如果这是你的用例,你当然可以写一个来做到这一点。
  • 使用 All 是个坏主意 - 如果其中任何一个 Func 返回 false,则执行将结束。
  • 是的,这是不使用它的另一个重要原因。
  • 这不是偷懒,是吗?所以它会立即评估查询。与普通的 foreach 相比没有真正的优势。
【解决方案3】:

List 中有一个 ForEach 方法。您可以通过调用 .ToList() 方法将 Enumerable 转换为 List,然后调用 ForEach 方法。

另外,我听说有人从 IEnumerable 中定义了自己的 ForEach 方法。这可以通过本质上调用 ForEach 方法来完成,而是将其包装在扩展方法中:

public static class IEnumerableExtensions
{
    public static IEnumerable<T> ForEach<T>(this IEnumerable<T> _this, Action<T> del)
    {
        List<T> list = _this.ToList();
        list.ForEach(del);
        return list;
    }
}

【讨论】:

  • 这将强制一个完整的序列评估占用临时列表的每个元素的内存。我会在任何时候使用 IEnumerable 上的扩展方法来尽可能长时间地保留惰性求值属性。
【解决方案4】:

您不能立即使用 LINQ 和 IEnumerable 执行此操作 - 您需要实现自己的扩展方法,或者使用 LINQ 将枚举转换为数组,然后调用 Array.ForEach():

Array.ForEach(MyCollection.ToArray(), x => x.YourMethod());

请注意,由于值类型和结构的工作方式,如果集合是值类型并且您以这种方式修改集合的元素,它将不会影响原始集合的元素。

【讨论】:

    【解决方案5】:

    不幸的是,在当前版本的 LINQ 中没有内置的方法可以做到这一点。框架团队忽略了添加 .ForEach 扩展方法。现在在下面的博客上有一个很好的讨论。

    http://blogs.msdn.com/kirillosenkov/archive/2009/01/31/foreach.aspx

    不过添加一个相当容易。

    public static void ForEach<T>(this IEnumerable<T> enumerable, Action<T> action) {
      foreach ( var cur in enumerable ) {
        action(cur);
      }
    }
    

    【讨论】:

    • 为了能够在串联的 LinQ 序列中使用它,请考虑将枚举返回为 IEnumerable。例如:...Select(...).ForEach(...).Where(...).Distinct();
    【解决方案6】:

    因为 LINQ 被设计为查询功能而不是更新功能,所以您不会找到在 IEnumerable 上执行方法的扩展,因为这将允许您执行方法(可能会产生副作用)。在这种情况下,你不妨坚持

    foreach(名称中的字符串名称)
    Console.WriteLine(name);

    【讨论】:

      【解决方案7】:

      一种快速简便的方法是:

      Names.ToList().ForEach(e => ...);
      

      【讨论】:

      • ToList() 将评估 IEnumerable 中的每个节点,这意味着在该点完成任何惰性评估。我宁愿通过制作自己的扩展方法来尽可能晚地离开它。
      • 对不起,我选择了不受欢迎的方法。我理解失去惰性评估的不利影响,但是当我在列表中执行 ForEach 时,无论如何我希望它现在发生,因此不再有太大的优势。
      • 扩展方法更好,而且它可能也更快,但这对我来说“有点”更标准,而且足够短,可以打字。谢谢杰!
      • 这有很大的问题。如果您执行 ToList,则所有对象必须同时存在于内存中。如果源是数据库,那么至少使用 IEnumerable 您的对象可能会在完成后被卸载。使用 ToList,您可能会耗尽大型数据集的内存!不要使用它!
      • 是的,这种方法有很多缺点,但我很惊讶它们经常不是问题,而且这种简单的方法效果很好。事实上,我的大部分序列都是
      【解决方案8】:

      好吧,你也可以使用标准的foreach 关键字,只需将其格式化为oneliner:

      foreach(var n in Names.Where(blahblah)) DoStuff(n);
      

      抱歉,我认为这个选项值得在这里 :)

      【讨论】:

        【解决方案9】:

        使用并行 Linq:

        Names.AsParallel().ForAll(name => ...)

        【讨论】:

          【解决方案10】:

          如前所述,ForEach 扩展将进行修复。

          我对当前问题的提示是如何执行迭代器

          [我确实尝试过 Select(s=> { Console.WriteLine(s); return s; }),但它没有打印任何东西。]

          检查一下

          _= Names.Select(s=> { Console.WriteLine(s); return 0; }).Count();

          试试吧!

          【讨论】:

            猜你喜欢
            • 2010-12-15
            • 2016-08-03
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2021-07-14
            • 1970-01-01
            • 1970-01-01
            • 2015-04-19
            相关资源
            最近更新 更多