为什么 .ForEach() 方法在 IList<T> 上而不是 IEnumerable<T> 上?

55

可能是重复问题:
为什么在IEnumerable接口上没有ForEach扩展方法?

我发现当编写LINQ代码时,使用.ForEach()是一个很好的习惯。例如,下面这段代码会将以下输入转换为输出:

{ "One" } => "One"
{ "One", "Two" } => "One, Two"
{ "One", "Two", "Three", "Four" } => "One, Two, Three and Four";

这是代码:

private string InsertCommasAttempt(IEnumerable<string> words)
{
    List<string> wordList = words.ToList();
    StringBuilder sb = new StringBuilder();
    var wordsAndSeparators = wordList.Select((string word, int pos) =>
        {
            if (pos == 0) return new { Word = word, Leading = string.Empty };
            if (pos == wordList.Count - 1) return new { Word = word, Leading = " and " };
            return new { Word = word, Leading = ", " };
        });

    wordsAndSeparators.ToList().ForEach(v => sb.Append(v.Leading).Append(v.Word));
    return sb.ToString();
}

注意在倒数第二行的.ForEach()之前插入的.ToList()

为什么.ForEach()不是IEnumerable<T>上的扩展方法?像这样的示例,它似乎有点奇怪。


9
它甚至没有包含在IList<T>接口中,但是被List<T>实现所包括。 - Marc Wittke
3
问题是IEnumerable<T>和可能的IList<T>都是惰性的,所以在使用.ForEach之后仍然需要调用.ToList()或.ToArray()方法来确保实际遍历了所有项。如果值没有被使用,那么IEnumerable的惰性求值会导致你认为已经中止的.ForEach实际上并未停止。这就是为什么也有一个Array.ForEach的原因,因为一旦你调用ToArray()方法,编译器就可以确定列表已经被完全遍历。但对于ForEach<T>(this IEnumerable<T>...),你无法确定遍历是否已经完成。 - Jim
10个回答

40

因为 ForEach(Action)IEnumerable<T> 之前存在。

由于它没有与其他扩展方法一起添加,可以假设C#设计师认为这是一个糟糕的设计,更喜欢 foreach 结构。


编辑:

如果您想要的话,可以创建自己的扩展方法,它不会覆盖List<T> 的方法,但它将适用于实现 IEnumerable<T> 接口的任何其他类。

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

1
这并不妨碍他们在IEnumerable上放置一个foreach方法。 - Joel Coehoorn
3
不,但这表明他们认为该方法是错误的,更倾向于使用foreach结构。 - Samuel
这两个都可以作为我的答案。感谢您的建议。 - Olema
1
foreach循环中调用action()时,应该以item作为参数,而不是用T - RaYell

40
根据Eric Lippert的说法,这主要是出于哲学上的原因。你应该阅读整篇文章,但就我而言,这里是要点:
“我从哲学上反对提供这样的方法,理由有两个。第一个原因是这样做违背了所有其他序列运算符所基于的函数式编程原则。显然,调用此方法的唯一目的是引发副作用。表达式的目的是计算值,而不是引起副作用。语句的目的是引起副作用。这个东西的调用站点看起来非常像一个表达式(尽管,必须承认,由于该方法是void-returning,表达式只能在“语句表达式”上下文中使用)。让我感到不舒服的是,使唯一一个仅有用于其副作用的序列运算符。 第二个原因是这样做不会给语言增加任何新的表现力。”

9
如果你的个人编程偏好能够影响到数百万人,那不是很棒吗?这个"Eric Lippert"拥有如此大的权力... - Frank Krueger
13
是的,如果我们每个人都能设计一款被数百万人使用的编译器就好了 :) - Justin R.
但是这样我们就得不到索引了,只能使用普通的for循环,对吧? - JoeB
1
这是一个很好的解释,说明为什么该方法不存在,以及为什么你可能要三思而后行地自己添加它。 - tarrball

5

ForEach() 在 IEnumerable 上只是普通的 for each 循环,就像这样:

for each T item in MyEnumerable
{
    // Action<T> goes here
}

1
我能理解在ForEach中嵌入lambda而不是这样做的吸引力。但这似乎只是一个小问题。 - Promit
能不能像列表一样内联地完成这个操作会很好吧?我以前也曾想过这个问题,但从来没有想过提出来。 - BenAlabaster
是的,那也是我的想法。 - Olema
1
你唯一真正的答案是:因为。出于某种原因,设计师决定不将其添加到IEnumerable<T>中。但这并不妨碍你自己实现它,如果你愿意的话。 - Samuel
@balabaster: "你可以使用foreach循环遍历列表类型" ... 这是因为这些列表类型满足实现IEnumerable的要求。 - Joel Coehoorn
显示剩余2条评论

3

ForEach方法不是在IList接口中,而是在List类中。你在示例中使用的是具体的List类。


3
我猜测,在IEnumerable上使用foreach会使其操作具有副作用。没有任何“可用的”扩展方法会产生副作用,因此在其中添加类似于foreach这样的命令式方法可能会使API变得混乱。此外,foreach将初始化惰性集合。
个人而言,我一直在抵制诱惑,只是为了将无副作用的函数与具有副作用的函数分开,而不是简单地添加自己的函数。

0

老实说,我不确定为什么IEnumerable没有包含.ForEach(Action)方法,无论对错与否,事实就是这样...

然而,我确实想强调其他评论中提到的性能问题。根据你如何循环遍历集合,会有一定的性能损耗。虽然相对较小,但它确实存在。下面是一个非常快速且粗糙的代码片段,用于展示这种关系...只需要大约一分钟的时间来运行。

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Start Loop timing test: loading collection...");
        List<int> l = new List<int>();

        for (long i = 0; i < 60000000; i++)
        {
            l.Add(Convert.ToInt32(i));
        }

        Console.WriteLine("Collection loaded with {0} elements: start timings",l.Count());
        Console.WriteLine("\n<===============================================>\n");
        Console.WriteLine("foreach loop test starting...");

        DateTime start = DateTime.Now;

        //l.ForEach(x => l[x].ToString());

        foreach (int x in l)
            l[x].ToString();

        Console.WriteLine("foreach Loop Time for {0} elements = {1}", l.Count(), DateTime.Now - start);
        Console.WriteLine("\n<===============================================>\n");
        Console.WriteLine("List.ForEach(x => x.action) loop test starting...");

        start = DateTime.Now;

        l.ForEach(x => l[x].ToString());

        Console.WriteLine("List.ForEach(x => x.action) Loop Time for {0} elements = {1}", l.Count(), DateTime.Now - start);
        Console.WriteLine("\n<===============================================>\n");

        Console.WriteLine("for loop test starting...");

        start = DateTime.Now;
        int count = l.Count();
        for (int i = 0; i < count; i++)
        {
            l[i].ToString();
        }

        Console.WriteLine("for Loop Time for {0} elements = {1}", l.Count(), DateTime.Now - start);
        Console.WriteLine("\n<===============================================>\n");

        Console.WriteLine("\n\nPress Enter to continue...");
        Console.ReadLine();
    }

不要过于纠结这个问题。性能是应用程序设计的货币,但除非您的应用程序正在遭受实际的性能损失,导致可用性问题,否则请专注于编写易于维护和重用的代码,因为时间是现实生活业务项目的货币...

0

仅仅是猜测,但是List可以在不创建枚举器的情况下遍历其项:

public void ForEach(Action<T> action)
{
    if (action == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    for (int i = 0; i < this._size; i++)
    {
        action(this._items[i]);
    }
}

这可以带来更好的性能。使用IEnumerable,您无法选择使用普通的for循环。


在几乎所有情况下,性能损失将是可以忽略不计的。当然,你不能使用for循环,因为IEnumerable没有索引的概念。 - Samuel
它确实可以有所不同。我曾经遇到过这样的情况,从“foreach”改为“for”只是因为垃圾收集器不再需要收集枚举器,就导致速度提高了10倍。 - Rauhotz
1
我对此提出异议。要么展示证据,要么你的循环有非常严重的问题。 - Samuel
1
@Rauhotz,你的枚举器一定很糟糕。 - Rex M
什么糟糕的枚举器?它是从List<T>.GetEnumerator()返回的一个对象,必须由GC创建和收集。在高度多线程应用程序中的紧密循环中使用它,您将看到GC占用90%的CPU时间。当使用List<T>.Foreach时,不会创建任何对象,这可能会产生差异。 - Rauhotz

0

ForEach 在具体类 List<T> 中实现


3
可以将扩展方法添加到接口中。 - Rex M
Rex再次用负面评论拯救了我们!是的,我的跟踪者又回来了。 - Chad Grant
1
那怎么是一个负面评论呢? - Rex M
2
@Rex:既然他编辑了它,你的评论看起来就不对了。但最初它说你不能在接口上有扩展方法,这是非常错误的。 - Samuel
我承认我匆忙中发表了错误的声明并进行了更正。通常我会避免使用扩展方法,因为它们会像运算符重载一样使代码变得复杂...很难“知道”它们的存在。所以我在使用它们方面经验不太丰富。 - Chad Grant
显示剩余5条评论

0

LINQ 遵循拉模型,所有的(扩展)方法都应该返回 IEnumerable<T>,除了 ToList()ToList() 用于结束拉链。

ForEach() 来自推模型世界。

你仍然可以编写自己的扩展方法来实现这一点,正如 Samuel 指出的那样。


-1

IEnumerable<T> 上,它被称为 "Select" 我受到启发了,谢谢。


哎呀!我错了吗?请 enlighten 我! - JP Alioto
并不完全正确 - 在上面的例子中,wordsAndSeparators.Select(v =>sb.Append(v.Leading).Append(v.Word));由于管道处理,无法得到您所期望的结果。因此,您需要这样写:wordsAndSeparators.Select(v => sb.Append(v.Leading).Append(v.Word)).ToList(); - Olema
1
ForEach 接受一个 Action<T>,它是一个吃掉 T 并返回 void 的委托;因此,ForEach 返回 void。Select 接受一个 Func<T, TResult>,它是一个吃掉 T 并返回 TResults 的委托;因此,Select 返回一个 IEnumerable<TResult>。 - jason

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