为什么IEnumerable上没有ForEach扩展方法?


389

受另一个询问缺少Zip功能的问题的启发:

为什么班上没有ForEach扩展方法Enumerable?还是任何地方?唯一获得ForEach方法的类是List<>。是否有其丢失(性能)的原因?


它可能如先前的文章所暗示的那样,因为foreach在C#和VB.NET(以及许多其他语言)中作为语言关键字得到支持。大多数人(包括我自己)只是根据需要和适当的情况自己编写一个。
chrisb

2
这里的相关问题,带有指向“官方答案”的链接stackoverflow.com/questions/858978/…– Benjol
2009年


我认为非常重要的一点是,foreach循环更容易发现。如果使用.ForEach(...),则在浏览代码时很容易错过这一代码。当您遇到性能问题时,这变得很重要。当然,.ToList()和.ToArray()都有相同的问题,但使用方式略有不同。另一个原因可能是,在.ForEach中修改源列表(删除/添加元素)更为常见,因此它不再是“纯”查询。
DanielBişar2014年

当调试并行化代码,我经常尝试换 Parallel.ForEachEnumerable.ForEach只找回后者不存在。C#错过了使事情变得容易的技巧。
Panic Panic'6

Answers:


198

语言中已经包含了一个foreach语句,该语句在大多数情况下会起作用。

我不愿看到以下内容:

list.ForEach( item =>
{
    item.DoSomething();
} );

代替:

foreach(Item item in list)
{
     item.DoSomething();
}

后者在大多数情况下更清晰易读,尽管键入时间可能更长一些。

但是,我必须承认我在这个问题上改变了立场。在某些情况下,ForEach()扩展方法确实很有用。

这是语句和方法之间的主要区别:

  • 类型检查:foreach在运行时完成,ForEach()在编译时(Big Plus!)
  • 调用委托的语法确实简单得多:objects.ForEach(DoSomething);
  • 可以将ForEach()链接起来:尽管这种功能的弊端/使用尚待讨论。

这些都是许多人在这里提出的要点,我可以理解为什么人们缺少此功能。我不介意Microsoft在下一个框架迭代中添加标准的ForEach方法。


39
此处的讨论给出了答案:forums.microsoft.com/MSDN/…基本上,已决定使扩展方法在功能上保持“纯”。使用扩展方法时,ForEach会产生副作用,这不是目的。
mancaus

17
如果您具有C背景,则“ foreach”可能看起来更清晰,但是内部迭代会更清楚地表明您的意图。这意味着您可以执行“ sequence.AsParallel()。ForEach(...)”之类的操作。
杰伊·巴祖兹

10
我不确定您关于类型检查的观点是否正确。foreach如果可枚举参数化,则在编译时检查类型。它只缺少针对非通用集合的编译时类型检查,就像假设的ForEach扩展方法一样。
理查德·普尔

49
扩展方法在具有点标记的LINQ链中非常有用,例如:col.Where(...).OrderBy(...).ForEach(...)。语法简单得多,没有多余的局部变量或foreach()中的长括号引起的屏幕污染。
JacekGorgoń2012年

22
“我不希望看到以下内容”-问题不是关于您的个人喜好,而是通过添加多余的换行符和一对多余的花括号,使代码更令人讨厌。不是那么可恶list.ForEach(item => item.DoSomething())。谁说这是清单?显而易见的用例是在链的末端。使用foreach语句,您必须将整个链放在之后in,并将要执行的操作放在该块内,这远不自然或一致。
Jim Balter

73

在LINQ之前添加了ForEach方法。如果添加ForEach扩展,由于扩展方法的限制,它将永远不会被List实例调用。我认为未添加它的原因是不干扰现有的。

但是,如果您真的错过了这个小巧的功能,则可以推出自己的版本

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

11
扩展方法可以做到这一点。如果已经存在给定方法名称的实例实现,则使用该方法代替扩展方法。因此,您可以实现扩展方法ForEach,而不会使其与List <>。ForEach方法发生冲突。
卡梅隆·麦克法兰

3
Cameron,相信我,我知道扩展方法是如何工作的:) List继承了IEnumerable,所以这不是显而易见的事情。
aku

9
从扩展方法编程指南(msdn.microsoft.com/zh-cn/library/bb383977.aspx):永远不会调用与接口或类方法具有相同名称和签名的扩展方法。对我来说似乎很明显。
卡梅隆·麦克法兰

3
我在说不同的话吗?我认为许多类都具有ForEach方法不是很好,但是其中一些可能会以不同的方式工作。
aku

5
有关List <>。ForEach和Array.ForEach的一件值得注意的事情是,它们实际上并未在内部使用foreach构造。它们都使用普通的for循环。也许这是性能问题(diditwith.net/2006/10/05/PerformanceOfForeachVsListForEach.aspx)。但是鉴于此,Array和List都实现了ForEach方法,令人惊讶的是,它们至少没有为IList <>实现扩展方法,甚至也没有为IEnumerable <>实现。
马特·多森

53

您可以编写以下扩展方法:

// Possibly call this "Do"
IEnumerable<T> Apply<T> (this IEnumerable<T> source, Action<T> action)
{
    foreach (var e in source)
    {
        action(e);
        yield return e;
    }
}

优点

允许链接:

MySequence
    .Apply(...)
    .Apply(...)
    .Apply(...);

缺点

在执行强制迭代之前,它实际上不会做任何事情。因此,不应调用它.ForEach()。您可以.ToList()在末尾编写,也可以编写此扩展方法:

// possibly call this "Realize"
IEnumerable<T> Done<T> (this IEnumerable<T> source)
{
    foreach (var e in source)
    {
        // do nothing
        ;
    }

    return source;
}

与可能发布的C#库的差异可能太大了;不熟悉您的扩展方法的读者将不知道如何编写您的代码。


1
您的第一个函数可以不使用'realize'方法,例如:{foreach (var e in source) {action(e);} return source;}
jjnguy,2010年

3
您不能只使用Select而不是Apply方法吗?在我看来,将动作应用于每个元素并产生它正是Select所做的。例如numbers.Select(n => n*2);
rasmusvhansen

1
我认为,Apply比为此目的使用Select更可读。在阅读Select语句时,我将其理解为“从内部获取东西”,而Apply则是“要做一些工作”。
Rob Lang博士,

2
@rasmusvhansen其实,Select()说函数应用到每一个元素,并返回其中函数的值Apply()表示一个适用Action于每个元素,并返回原来的元素。
NetMage

1
这等效于Select(e => { action(e); return e; })
Jim Balter

35

这里的讨论给出了答案:

实际上,我所看到的具体讨论实际上取决于功能的纯度。在表达式中,经常会假设没有副作用。拥有ForEach尤其会带来副作用,而不仅仅是忍受它们。-Keith Farmer(合作伙伴)

基本上,决定使扩展方法在功能上保持“纯”。使用Enumerable扩展方法时,ForEach会产生副作用,这不是目的。


6
另请参阅blogs.msdn.com/b/ericlippert/archive/2009/05/18/…。这样做违反了所有其他序列运算符所基于的功能编程原则。显然,调用此方法的唯一目的是引起副作用
Michael Freidgeim 2012年

7
这种推理是完全伪造的。该用例是需要的副作用......没有的ForEach,你必须写一个foreach循环。ForEach的存在不会助长副作用,而缺少它不会减少副作用。
吉姆·巴尔特

鉴于编写扩展方法非常简单,因此没有理由认为它不是语言标准。
普林尼船长

17

尽管我同意foreach在大多数情况下使用内置构造会更好,但是我发现在ForEach <>扩展中使用此变体比必须foreach自己定期管理索引要好一些:

public static int ForEach<T>(this IEnumerable<T> list, Action<int, T> action)
{
    if (action == null) throw new ArgumentNullException("action");

    var index = 0;

    foreach (var elem in list)
        action(index++, elem);

    return index;
}
var people = new[] { "Moe", "Curly", "Larry" };
people.ForEach((i, p) => Console.WriteLine("Person #{0} is {1}", i, p));

会给你:

Person #0 is Moe
Person #1 is Curly
Person #2 is Larry

很好的扩展,但是您可以添加一些文档吗?返回值的预期用途是什么?为什么索引是var而不是int?样品用法?
Mark T

标记:返回值是列表中元素的数量。指数具体类型为int,由于隐式类型。
克里斯·兹维里克

@Chris,根据您上面的代码,返回值是列表中最后一个值的从零开始的索引,而不是列表中元素的数量。
埃里克·史密斯

@Chris,不,不是:在获取值后索引增加(后缀增加),因此在处理完第一个元素后,索引将为1,依此类推。因此,返回值实际上就是元素计数。
MartinStettner

1
@MartinStettner Eric Smith在5/16上的评论来自早期版本。他更正了5/18上的代码以返回正确的值。
Michael Blackburn 2014年

16

一种解决方法是编写.ToList().ForEach(x => ...)

优点

易于理解-读者只需要知道C#附带的内容,而不需要任何其他扩展方法。

语法噪声非常轻微(仅添加了一些无关紧要的代码)。

通常不会花费额外的内存,因为本地人.ForEach()无论如何都必须实现整个集合。

缺点

操作顺序并不理想。我宁愿意识到一个要素,然后对其采取行动,然后重复。该代码首先实现所有元素,然后依次对它们进行操作。

如果实现该列表引发异常,则您永远不必对单个元素采取行动。

如果枚举是无限的(如自然数),那么您就不走运了。


1
a)这甚至不是问题的答案。b)如果前面提到没有Enumerable.ForEach<T>方法但有方法,则此响应会更加清楚List<T>.ForEach。c)“通常不会花费额外的内存,因为无论如何,本机.ForEach()必须实现整个集合。” -完全废话 这是C#如果提供的话,将提供Enumerable.ForEach <T>的实现:public static void ForEach<T>(this IEnumerable<T> list, Action<T> action) { foreach (T item in list) action(item); }
Jim Balter

13

我一直想知道自己,这就是为什么我总是随身携带这个:

public static void ForEach<T>(this IEnumerable<T> col, Action<T> action)
{
    if (action == null)
    {
        throw new ArgumentNullException("action");
    }
    foreach (var item in col)
    {
        action(item);
    }
}

不错的扩展方法。


2
col可以为null ...您也可以检查。
艾米B

是的,我确实要检查我的图书馆,但是在那儿快速检查时我只是想念它。
亚伦·鲍威尔,

我发现每个人都检查action参数是否为null,但从未检查source / col参数,这很有趣。
卡梅隆·麦克法兰

2
我觉得有趣的是每个人都检查参数为空,当一个异常没有检查结果,以及...
oɔɯǝɹ

一点也不。Null Ref异常不会告诉您有错误的参数的名称。而且您也可以在循环中进行检查,以便出于相同原因可以抛出特定的Null引用说col [index#]为null
Roger Willcocks 2015年

8

因此,对于ForEach扩展方法不合适,因为它不会像LINQ扩展方法那样返回值,因此有很多评论。尽管这是事实陈述,但并非完全正确。

LINQ扩展方法都返回一个值,因此可以将它们链接在一起:

collection.Where(i => i.Name = "hello").Select(i => i.FullName);

但是,仅仅因为LINQ是使用扩展方法实现的,并不意味着必须以相同的方式使用扩展方法并返回值。编写扩展方法以暴露不返回值的通用功能是一种完全有效的用法。

关于ForEach的特定论点是,基于对扩展方法的限制(即,扩展方法将永远不会覆盖具有相同签名的继承方法),可能存在这样的情况:自定义扩展方法可用于所有妨碍实现的类IEnumerable <T>“列表”除外<T。当方法根据调用扩展方法还是继承方法而开始表现不同时,这可能引起混乱。


7

您可以使用(可链接,但懒惰地求值)Select,首先进行操作,然后返回身份(或者您愿意的其他方式)

IEnumerable<string> people = new List<string>(){"alica", "bob", "john", "pete"};
people.Select(p => { Console.WriteLine(p); return p; });

您将需要使用Count()(最便宜的枚举afaik的操作)或无论如何需要的其他操作来确保仍对其进行评估。

我很乐意将其引入标准库:

static IEnumerable<T> WithLazySideEffect(this IEnumerable<T> src, Action<T> action) {
  return src.Select(i => { action(i); return i; } );
}

上面的代码然后变成people.WithLazySideEffect(p => Console.WriteLine(p))实际上等效于foreach的代码,但是是惰性的和可链接的。


太棒了,它从未点击返回的选择将像这样工作。很好地使用了方法语法,因为我不认为您可以使用表达式语法来做到这一点(因此我以前从未想到过这种方法)。
Alex KeySmith

@AlexKeySmith如果C#具有序列运算符(例如其前身),则可以使用表达式语法来实现。但是ForEach的全部要点是它对void类型执行操作,并且您不能在C#的表达式中使用void类型。
吉姆·巴尔特

恕我直言,现在Select不再执行应做的事情了。这绝对是OP所要求的解决方案-但关于主题的可读性:没有人会期望select会执行功能。
Matthias Burger

@MatthiasBurger如果Select不执行应执行的操作Select,则是实现的问题,而不是调用的代码的问题,这对执行的操作Select没有影响Select
马丁


4

@钱币

foreach扩展方法的真正强大之处在于Action<>无需在代码中添加不必要的方法即可重用。假设您有10个列表,并且要对它们执行相同的逻辑,并且相应的函数不适合您的类,因此不会被重用。您可以将所有逻辑放在一个位置(而不是使用十个for循环或一个显然不属于辅助函数的通用函数)(Action<>因此,数十行被替换为

Action<blah,blah> f = { foo };

List1.ForEach(p => f(p))
List2.ForEach(p => f(p))

等等...

逻辑合二为一,您没有污染课堂。


foreach(List1.Concat(List2).Concat(List3)中的var p){...做东西...}
Cameron MacFarland

如果很简单f(p),请省略{ }速记:for (var p in List1.Concat(List2).Concat(List2)) f(p);
spoulson 2010年

3

大多数LINQ扩展方法都返回结果。ForEach不适合此模式,因为它不返回任何内容。


2
ForEach使用Action <T>,这是代表的另一种形式
Aaron Powell,

2
除了leppie的权利外,所有Enumerable扩展方法都返回一个值,因此ForEach不适合该模式。代表们没有参加。
卡梅隆·麦克法兰

只是扩展方法不必返回结果,它的目的是添加一种调用常用操作的方法
Aaron Powell

1
的确是斯莱克斯。那么,如果它不返回值怎么办?
TraumaPony

1
@Martijn iepublic IEnumerable<T> Foreach<T>(this IEnumerable<T> items, Action<T> action) { /* error check */ foreach (var item in items) { action(item); yield return item; } }
McKay

3

如果您有F#(它将在.NET的下一版本中),则可以使用

Seq.iter doSomething myIEnumerable


9
因此,Microsoft选择不为C#和VB中的IEnumerable提供ForEach方法,因为它不是纯粹的功能。但是,他们用功能更强大的语言F#DID提供了它(命名为iter)。他们的逻辑暗示了我。
Scott Hutchinson

3

部分原因是语言设计者从哲学角度不同意它。

  • 没有(和测试...)功能要比拥有功能少。
  • 它并不是真的更短(有一些传递函数的情况,但这不是主要用途)。
  • 目的是要产生副作用,这不是linq所要解决的。
  • 为什么还有另一种方法可以完成我们已经拥有的功能呢?(foreach关键字)

https://blogs.msdn.microsoft.com/ericlippert/2009/05/18/foreach-vs-foreach/


2

您可以在需要返回内容时使用select。如果不这样做,则可以先使用ToList,因为您可能不想修改集合中的任何内容。


a)这不是问题的答案。b)ToList需要额外的时间和内存。
Jim Balter

2

我写了一篇关于它的博客文章:http : //blogs.msdn.com/kirillosenkov/archive/2009/01/31/foreach.aspx

如果您想在.NET 4.0中看到此方法,可以在这里投票:http : //connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=279093


这曾经实施过吗?我想不是,但是还有其他URL可以代替该票证吗?MS连接已中止
未知

这没有实现。考虑在github.com/dotnet/runtime/issues
Kirill Osenkov


2

是我还是List <T>。Linq几乎将Foreach淘汰了。最初有

foreach(X x in Y) 

其中Y只需是IEnumerable(Pre 2.0),并实现GetEnumerator()。如果您查看生成的MSIL,您会发现它与

IEnumerator<int> enumerator = list.GetEnumerator();
while (enumerator.MoveNext())
{
    int i = enumerator.Current;

    Console.WriteLine(i);
}

(有关MSIL,请参见http://alski.net/post/0a-for-foreach-forFirst-forLast0a-0a-.aspx

然后在DotNet2.0中出现了泛型和列表。我一直认为Foreach是Vistor模式的实现,(请参阅Gamma,Helm,Johnson和Vlissides的设计模式)。

当然,现在在3.5版本中,我们可以改为使用Lambda达到相同的效果,例如,请尝试 http://dotnet-developments.blogs.techtarget.com/2008/09/02/iterators-lambda-and-linq-oh-我/


1
我相信为“ foreach”生成的代码应该有一个try-finally块。因为T的IEnumerator继承自接口IDisposable。因此,在生成的finally块中,应处理对象T的IEnumerator。
Morgan Cheng

2

我想扩展一下Aku的答案

如果您要仅出于副作用的目的而调用方法,而无需首先遍历整个可枚举,则可以使用以下方法:

private static IEnumerable<T> ForEach<T>(IEnumerable<T> xs, Action<T> f) {
    foreach (var x in xs) {
        f(x); yield return x;
    }
}

1
这与Select(x => { f(x); return x;})
Jim Balter

0

还没有人指出ForEach <T>会导致编译时类型检查,其中foreach关键字是运行时检查的。

在对两种方法都在代码中使用的地方进行了一些重构之后,我偏爱.ForEach,因为我必须找出测试失败/运行时失败来查找foreach问题。


5
真的是这样吗?我看不到如何foreach减少编译时间检查。当然,如果您的集合是非泛型IEnumerable,则循环变量将为object,但对于假设的ForEach扩展方法也是如此。
理查德·普尔

1
已接受的答案中提到了这一点。但是,这是完全错误的(上面的Richard Poole是正确的)。
吉姆·巴尔特
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.