ChatGPT解决这个技术问题 Extra ChatGPT

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

受到另一个关于缺少 Zip 函数的问题的启发:

为什么 IEnumerable 接口上没有 ForEach 扩展方法?还是在任何地方?获得 ForEach 方法的唯一类是 List<>。是否有它丢失的原因,也许是性能?

它可能正如之前的帖子所暗示的那样,因为在 C# 和 VB.NET(以及许多其他人)中支持 foreach 作为语言关键字大多数人(包括我自己)只是根据需要和适当的方式自己编写一个。
此处带有“官方答案”链接的相关问题stackoverflow.com/questions/858978/…
我认为非常重要的一点是,foreach 循环更容易被发现。如果您使用 .ForEach(...) 很容易错过这个,当您查看代码时。当您遇到性能问题时,这变得很重要。当然 .ToList() 和 .ToArray() 有相同的问题,但它们的使用方式略有不同。另一个原因可能是在 .ForEach 中更常见的是修改源列表(删除/添加元素),因此它不再是“纯”查询。
在调试并行代码时,我经常尝试将 Parallel.ForEach 换成 Enumerable.ForEach,只是为了重新发现后者不存在。 C# 在这里错过了一个让事情变得简单的技巧。

S
Sergey

大部分时间完成这项工作的语言中已经包含一个 foreach 语句。

我不想看到以下内容:

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

代替:

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

后者在大多数情况下更清晰、更容易阅读,尽管打字时间可能会更长一些。

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

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

类型检查: foreach 在运行时完成, ForEach() 在编译时完成(大加!)

调用委托的语法确实简单得多:objects.ForEach(DoSomething);

ForEach() 可以被链接:尽管这种功能的邪恶/有用性有待讨论。

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


此处的讨论给出了答案:forums.microsoft.com/MSDN/… 基本上,我们决定保持扩展方法在功能上“纯”。 ForEach 在使用扩展方法时会鼓励副作用,这不是本意。
如果您有 C 背景,“foreach”可能看起来更清晰,但内部迭代更清楚地说明了您的意图。这意味着您可以执行“sequence.AsParallel().ForEach(...)”之类的操作。
我不确定您关于类型检查的观点是否正确。 foreach 如果可枚举参数化,则在编译时进行类型检查。它只缺少对非泛型集合的编译时类型检查,假设的 ForEach 扩展方法也是如此。
扩展方法在带有点符号的链 LINQ 中非常有用,例如:col.Where(...).OrderBy(...).ForEach(...)。更简单的语法,没有多余的局部变量或 foreach() 中的长括号造成的屏幕污染。
“我不想看到以下内容”——问题与您的个人喜好无关,您通过添加无关的换行符和一对无关的大括号使代码更加可恨。 list.ForEach(item => item.DoSomething()) 没有那么可恨。谁说这是一个列表?明显的用例位于链的末端;使用 foreach 语句,您必须将整个链放在 in 之后,并且在块内执行操作,这远不自然或一致。
a
aku

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

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

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

扩展方法允许这样做。如果已经存在给定方法名称的实例实现,则使用该方法而不是扩展方法。因此,您可以实现一个扩展方法 ForEach 而不会与 List<>.ForEach 方法发生冲突。
Cameron,相信我,我知道扩展方法是如何工作的 :) List 继承 IEnumerable 所以这不是显而易见的事情。
来自扩展方法编程指南 (msdn.microsoft.com/en-us/library/bb383977.aspx):永远不会调用与接口或类方法具有相同名称和签名的扩展方法。对我来说似乎很明显。
我在说什么不同的东西吗?我认为当许多类都有 ForEach 方法但其中一些可能会以不同的方式工作时,这并不好。
关于 List<>.ForEach 和 Array.ForEach 的一件有趣的事情是它们实际上并没有在内部使用 foreach 构造。他们都使用普通的 for 循环。也许这是一个性能问题(diditwith.net/2006/10/05/PerformanceOfForeachVsListForEach.aspx)。但鉴于 Array 和 List 都实现了 ForEach 方法,令人惊讶的是,如果不是 IEnumerable<> 的话,它们至少没有实现 IList<> 的扩展方法。也。
J
Jay Bazuzi

您可以编写此扩展方法:

// 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# 库相差太大;不熟悉您的扩展方法的读者将不知道如何处理您的代码。


如果写成这样,您的第一个函数可以在没有“实现”方法的情况下使用:{foreach (var e in source) {action(e);} return source;}
您不能只使用 Select 而不是 Apply 方法吗?在我看来,对每个元素应用一个动作并产生它正是 Select 所做的。例如numbers.Select(n => n*2);
我认为 Apply 比为此目的使用 Select 更具可读性。在阅读 Select 语句时,我将其解读为“从内部获取一些东西”,而 Apply 是“做一些工作”。
@rasmusvhansen 实际上,Select() 表示将函数应用于每个元素并返回函数的值,其中 Apply() 表示将 Action 应用于每个元素并返回 original 元素。
这相当于 Select(e => { action(e); return e; })
s
sashoalm

The discussion here 给出了答案:

实际上,我目睹的具体讨论确实取决于功能纯度。在表达式中,经常会假设没有副作用。拥有 ForEach 会特别引起副作用,而不仅仅是忍受它们。 ——基思·法默(合伙人)

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


另见blogs.msdn.com/b/ericlippert/archive/2009/05/18/…。这样做违反了所有其他序列运算符所基于的函数式编程原则。显然,调用此方法的唯一目的是引起副作用
这种推理完全是假的。用例是需要副作用......没有 ForEach,您必须编写一个 foreach 循环。 ForEach 的存在不会鼓励副作用,它的缺席也不会减少副作用。
鉴于编写扩展方法是多么简单,实际上没有理由说它不是语言标准。
@MichaelFreidgeim 现已失效的链接存档:web.archive.org/web/20151205151706/http://blogs.msdn.com/b/…
C
Chris Zwiryk

虽然我同意在大多数情况下最好使用内置的 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?样品使用?
标记:返回值是列表中元素的数量。多亏了隐式类型,索引被专门键入为 int。
@Chris,根据您上面的代码,返回值是列表中最后一个值的从零开始的索引,而不是列表中元素的数量。
@Chris,不,不是:索引在取值后递增(后缀递增),因此在处理第一个元素后,索引将为1,依此类推。所以返回值实际上是元素计数。
@MartinStettner Eric Smith 于 5/16 发表的评论来自早期版本。他在 5/18 上更正了代码以返回正确的值。
J
Jay Bazuzi

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

优点

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

句法噪音非常温和(仅添加了一些无关代码)。

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

缺点

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

如果意识到列表引发异常,您将永远无法对单个元素采取行动。

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


a)这甚至不是问题的答案。 b) 如果前面提到,虽然没有 Enumerable.ForEach<T> 方法,但有 List<T>.ForEach 方法,这个响应会更清楚。 c)“通常不会花费额外的内存,因为本机 .ForEach() 无论如何都必须实现整个集合。” ——完全的胡说八道。这是 Enumerable.ForEach<T> 的实现。如果这样做,C# 会提供:public static void ForEach<T>(this IEnumerable<T> list, Action<T> action) { foreach (T item in list) action(item); }
A
Alexandr Nikitin

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

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

不错的小扩展方法。


col 可能为空......你也可以检查一下。
是的,我确实检查了我的图书馆,我只是在那里快速完成时错过了它。
我发现有趣的是,每个人都检查 action 参数是否为 null,但从不检查 source/col 参数。
我发现有趣的是每个人都检查参数是否为空,而不检查结果也会导致异常......
一点也不。 Null Ref 异常不会告诉您出错的参数的名称。你也可以检查循环,这样你就可以抛出一个特定的 Null Ref 说 col[index#] 是 null 出于同样的原因
S
Scott Dorman

因此,有很多关于 ForEach 扩展方法不合适的评论,因为它不像 LINQ 扩展方法那样返回值。虽然这是一个事实陈述,但并不完全正确。

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

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

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

关于 ForEach 的具体论点是,基于对扩展方法的约束(即扩展方法从不覆盖具有相同签名的继承方法),可能存在自定义扩展方法在所有 impelement IEnumerable<T> 的类上都可用的情况;除了列表<T>。当方法开始表现不同时,这可能会导致混淆,具体取决于是否调用了扩展方法或继承方法。


J
Jim Balter

您可以使用(可链接,但延迟评估)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,但是惰性且可链接。


太棒了,它从来没有点击返回的选择会像这样工作。很好地使用了方法语法,因为我认为你不能用表达式语法来做到这一点(因此我以前没有这样想过)。
@AlexKeySmith 如果 C# 有一个序列运算符,就像它的前辈一样,你可以用表达式语法来做到这一点。但是 ForEach 的全部意义在于它采用 void 类型的操作,并且您不能在 C# 的表达式中使用 void 类型。
恕我直言,现在 Select 不再做它应该做的事情了。这绝对是 OP 所要求的解决方案 - 但在主题可读性方面:没有人会期望 select 确实执行一个函数。
@MatthiasBurger 如果 Select 没有做它应该做的事情,那是 Select 实现的问题,而不是调用 Select 的代码,它对 Select 的作用没有影响。
D
Dave Clausen

请注意,MoreLINQ NuGet 提供了您正在寻找的 ForEach 扩展方法(以及执行委托并产生其结果的 Pipe 方法)。看:

https://www.nuget.org/packages/morelinq

https://code.google.com/p/morelinq/wiki/OperatorsOverview


s
spoulson

@币币

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

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

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

ETC...

逻辑在一个地方,你没有污染你的班级。


foreach (var p in List1.Concat(List2).Concat(List3)) { ... 做事... }
如果它像 f(p) 一样简单,则省略 { } 的简写:for (var p in List1.Concat(List2).Concat(List2)) f(p);
l
leppie

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


ForEach 使用 Action,这是另一种形式的委托
除了 leppie 的权利,所有 Enumerable 扩展方法都返回一个值,因此 ForEach 不适合该模式。代表们不会参与其中。
只是扩展方法不必返回结果,它的目的是添加一种调用常用操作的方法
确实,斯莱斯。那么如果它没有返回值怎么办?
@Martijn 即 public IEnumerable<T> Foreach<T>(this IEnumerable<T> items, Action<T> action) { /* error check */ foreach (var item in items) { action(item); yield return item; } }
v
vzwick

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

Seq.iter doSomething myIEnumerable


所以微软选择不在 C# 和 VB 中为 IEnumerable 提供 ForEach 方法,因为它不是纯粹的函数式;然而,他们确实以更实用的语言 F# 提供了它(命名为 iter)。他们的逻辑暗示了我。
M
McKay

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

没有(和测试......)一个功能比拥有一个功能要少。

它并不是真的更短(有一些传递函数的情况,但这不是主要用途)。

它的目的是产生副作用,这不是 linq 的意义所在。

为什么要用另一种方法来做与我们已经拥有的功能相同的事情? (foreach 关键字)

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


P
Paco

当你想返回某些东西时,你可以使用 select 。如果不这样做,您可以先使用 ToList,因为您可能不想修改集合中的任何内容。


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

我写了一篇关于它的博文: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 提交新问题
C
Community

在 3.5 中,添加到 IEnumerable 的所有扩展方法都用于 LINQ 支持(注意它们是在 System.Linq.Enumerable 类中定义的)。在这篇文章中,我解释了为什么 foreach 不属于 LINQ:Existing LINQ extension method similar to Parallel.For?


d
davidsleeps

是我还是 List.Foreach 几乎已经被 Linq 淘汰了。原来有

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-my/


我相信为“foreach”生成的代码应该有一个 try-finally 块。因为 T 的 IEnumerator 继承自接口 IDisposable。所以,在生成的 finally 块中,T 实例的 IEnumerator 应该是 Disposed 的。
C
Community

我想扩展 Aku's answer

如果你想调用一个方法只是为了它的副作用而不首先迭代整个可枚举,你可以使用这个:

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

这与 Select(x => { f(x); return x;}) 相同
L
Liran Barniv

我的版本是一种扩展方法,允许您在 T 的 IEnumerable 上使用 ForEach

public static class EnumerableExtension
{
        public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
    {
        source.All(x =>
        {
            action.Invoke(x);
            return true;
        });
    }
}

S
Squirrel

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

在代码中使用了这两种方法的地方进行了一些重构之后,我更喜欢 .ForEach,因为我必须寻找测试失败/运行时失败才能找到 foreach 问题。


真的是这样吗?我看不到 foreach 如何减少编译时间检查。当然,如果您的集合是非泛型 IEnumerable,则循环变量将是 object,但对于假设的 ForEach 扩展方法也是如此。
这在接受的答案中有所提及。然而,这完全是错误的(上面的 Richard Poole 是正确的)。