如何用 C# 中的函数语句替换 for 循环?
一位同事曾经说过,每次我写一个 for 循环时,上帝都在杀死一只小猫。
当被问及如何避免 for 循环时,他的答案是使用函数式语言。但是,如果您陷入非函数式语言(例如 C#)的困境,有哪些技术可以避免 for 循环或通过重构消除它们?也许使用 lambda 表达式和 LINQ?如果是这样,怎么办?
问题
所以问题归结为:
- 为什么 for 循环不好?或者,在什么情况下需要避免 for 循环以及为什么?
- 您能否提供 C# 代码示例来说明它之前(即使用循环)和之后不使用循环的情况?
A colleague once said that God is killing a kitten every time I write a for-loop.
When asked how to avoid for-loops, his answer was to use a functional language. However, if you are stuck with a non-functional language, say C#, what techniques are there to avoid for-loops or to get rid of them by refactoring? With lambda expressions and LINQ perhaps? If so, how?
Questions
So the question boils down to:
- Why are for-loops bad? Or, in what context are for-loops to avoid and why?
- Can you provide C# code examples of how it looks before, i.e. with a loop, and afterwards without a loop?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(18)
当您操作某些数据集并想要转换、过滤或聚合元素时,函数构造通常比 for 循环更清楚地表达您的意图。
当您想要重复执行某些操作时,循环非常合适。
例如,比
以下更清楚地表达您的意图
Functional constructs often express your intent more clearly than for-loops in cases where you operate on some data set and want to transform, filter or aggregate the elements.
Loops are very appropriate when you want to repeatedly execute some action.
For example
much more clearly expresses your intent than
如果您的同事有函数式编程,那么他可能已经熟悉避免 for 循环的基本原因:
折叠/映射/过滤涵盖了列表遍历的大多数用例,并且非常适合函数组合。 For 循环不是一个好的模式,因为它们不可组合。
大多数时候,您会遍历列表来折叠(聚合)、映射或过滤列表中的值。这些高阶函数已经存在于每种主流函数式语言中,因此您很少看到函数式代码中使用 for 循环习惯用法。
高阶函数是函数组合的基础,这意味着您可以轻松地将简单的函数组合成更复杂的函数。
举一个重要的例子,请考虑命令式语言中的以下内容:
在函数式语言中,我们会编写
map g (map fx)
,或者我们可以使用消除中间列表地图(f.g)x
。现在,原则上我们可以从命令式版本中消除中间列表,这会有所帮助,但效果不大。命令式版本的主要问题在于 for 循环是实现细节。如果你想改变这个函数,你就改变它的实现——最终你会修改很多代码。
举个例子,您将如何强制编写
map g (filter fx)
?好吧,由于您无法重用原始的映射和映射代码,因此您需要编写一个新的函数来进行过滤和映射。如果你有 50 种映射方式和 50 种过滤方式,你如何需要 50^50 个函数,或者你需要使用命令模式模拟将函数作为第一类参数传递的能力(如果你曾经尝试过函数式编程)在 Java 中,你就会明白这将是一场多么可怕的噩梦)。回到功能宇宙,您可以概括
map g (map fx)
,让您可以用filter
或filter
替换map
code>fold 根据需要:并使用
apply2 map g filter f
或apply2 map g map f
或apply2 filter g filter f
调用它代码> 或任何你需要的东西。现在,您可能永远不会在现实世界中编写这样的代码,您可能会使用以下方法来简化它:高阶函数和函数组合为您提供了命令式代码无法获得的抽象级别。
抽象出循环的实现细节可以让您无缝地将一个循环替换为另一个循环。
请记住,for 循环是一种实现细节。如果需要更改实现,则需要更改每个 for 循环。
映射/折叠/过滤抽象掉循环。因此,如果您想更改循环的实现,可以在这些函数中更改它。
现在您可能想知道为什么要抽象出循环。考虑将项目从一种类型映射到另一种类型的任务:通常,项目一次映射一个,顺序且独立于所有其他项目。大多数时候,像这样的映射是并行化的主要候选者。
不幸的是,顺序映射和并行映射的实现细节不可互换。如果您的代码中有大量顺序映射,并且希望将它们替换为并行映射,那么您有两种选择:在代码库中复制/粘贴相同的并行映射代码,或者将映射逻辑抽象为两个函数
map
和pmap
。一旦你选择了第二条路,你就已经深入函数式编程领域了。如果您了解函数组合的目的并抽象出实现细节(甚至像循环这样琐碎的细节),您就可以开始理解函数式编程如何以及为何如此强大。
If your colleague has a functional programming, then he's probably already familiar with the basic reasons for avoiding for loops:
Fold / Map / Filter cover most use cases of list traversal, and lend themselves well to function composition. For-loops aren't a good pattern because they aren't composable.
Most of the time, you traverse through a list to fold (aggregate), map, or filter values in a list. These higher order functions already exist in every mainstream functional language, so you rarely see the for-loop idiom used in functional code.
Higher order functions are the bread and butter of function composition, meaning you can easily combine simple function into something more complex.
To give a non-trivial example, consider the following in an imperative language:
In a functional language, we'd write
map g (map f x)
, or we can eliminate the intermediate list usingmap (f . g) x
. Now we can, in principle, eliminate the intermediate list from the imperative version, and that would help a little -- but not much.The main problem with the imperative version is simply that the for-loops are implementation details. If you want change the function, you change its implementation -- and you end up modifying a lot of code.
Case in point, how would you write
map g (filter f x)
in imperatively? Well, since you can't reuse your original code which maps and maps, you need to write a new function which filters and maps instead. And if you have 50 ways to map and 50 ways to filter, how you need 50^50 functions, or you need to simulate the ability to pass functions as first-class parameters using the command pattern (if you've ever tried functional programming in Java, you understand what a nightmare this can be).Back in the the functional universe, you can generalize
map g (map f x)
in way that lets you swap out themap
withfilter
orfold
as needed:And call it using
apply2 map g filter f
orapply2 map g map f
orapply2 filter g filter f
or whatever you need. Now you'd probably never write code like that in the real world, you'd probably simplify it using:Higher-order functions and function composition give you a level of abstraction that you cannot get with the imperative code.
Abstracting out the implementation details of loops let's you seamlessly swap one loop for another.
Remember, for-loops are an implementation detail. If you need to change the implementation, you need to change every for-loop.
Map / fold / filter abstract away the loop. So if you want to change the implementation of your loops, you change it in those functions.
Now you might wonder why you'd want to abstract away a loop. Consider the task of mapping items from one type to another: usually, items are mapped one at a time, sequentially, and independently from all other items. Most of the time, maps like this are prime candidates for parallelization.
Unfortunately, the implementation details for sequential maps and parallel maps aren't interchangeable. If you have a ton of sequential maps all over your code, and you want swap them out for parallel maps, you have two choices: copy/paste the same parallel mapping code all over your code base, or abstract away mapping logic into two functions
map
andpmap
. Once you're go the second route, you're already knee-deep in functional programming territory.If you understand the purpose of function composition and abstracting away implementation details (even details as trivial as looping), you can start to appreciate just how and why functional programming is so powerful in the first place.
比较以下内容:
与 foreach:
与 LINQ: 就
我个人而言,所有三个选项都有自己的位置,我都使用它们。这是使用最适合您的场景的选项的问题。
Compare the following:
vs foreach:
vs. LINQ:
Personally, all three options have their place, and I use them all. It's a matter of using the most appropriate option for your scenario.
for 循环没有任何问题,但以下是人们可能更喜欢函数式/声明式方法(如 LINQ)的一些原因,在这种方法中,您可以声明您想要的内容,而不是如何获得它:-
函数式方法可能更容易使用 PLINQ 手动并行化或由编译器。随着 CPU 转向更多内核,这可能会变得更加重要。
函数式方法可以更轻松地在多步骤过程中实现延迟求值,因为您可以将中间结果作为尚未完全求值的简单变量传递到下一步,而不是完全求值第一步然后传递下一步的集合(或者不使用单独的方法和yield语句来实现相同的程序)。
函数式方法通常更短且更易于阅读。
函数式方法通常会消除 for 循环中的复杂条件体(例如 if 语句和“继续”语句),因为您可以将 for 循环分解为逻辑步骤 - 选择所有匹配的元素,对它们执行操作,.. .
There's nothing wrong with for loops but here are some of the reasons people might prefer functional/declarative approaches like LINQ where you declare what you want rather than how you get it:-
Functional approaches are potentially easier to parallelize either manually using PLINQ or by the compiler. As CPUs move to even more cores this may become more important.
Functional approaches make it easier to achieve lazy evaluation in multi-step processes because you can pass the intermediate results to the next step as a simple variable which hasn't been evaluated fully yet rather than evaluating the first step entirely and then passing a collection to the next step (or without using a separate method and a yield statement to achieve the same procedurally).
Functional approaches are often shorter and easier to read.
Functional approaches often eliminate complex conditional bodies within for loops (e.g. if statements and 'continue' statements) because you can break the for loop down into logical steps - selecting all the elements that match, doing an operation on them, ...
For 循环不会杀人(或小猫、小狗或小东西)。人们杀人。
For 循环本身并不坏。然而,就像其他事情一样,使用它们的方式可能会很糟糕。
For loops don't kill people (or kittens, or puppies, or tribbles). People kill people.
For loops, in and of themselves, are not bad. However, like anything else, it's how you use them that can be bad.
有时你不只杀死一只小猫。
你会把他们都杀掉。
Sometime you don't kill just one kitten.
Sometimes you kill them all.
您可以充分重构代码,这样您就不会经常看到它们。一个好的函数名肯定比 for 循环更具可读性。
以 AndyC 中的示例:
Loop
Linq
无论您在函数中使用第一个还是第二个版本,它都更容易阅读
现在这是一个过于简单的示例,但您明白我的意思。
You can refactor your code well enough so that you won't see them often. A good function name is definitely more readable that a for loop.
Taking the example from AndyC :
Loop
Linq
Wheter you use the first or the second version inside your function, It's easier to read
Now that's an overly simple example, but you get my point.
你同事的说法不对。 For 循环本身并不坏。它们干净、可读并且不太容易出错。
Your colleague is not right. For loops are not bad per se. They are clean, readable and not particularly error prone.
你的同事关于 for 循环在所有情况下都很糟糕的观点是错误的,但正确的是它们可以在功能上重写。
假设您有一个如下所示的扩展方法:
然后您可以编写一个如下所示的循环:
这现在可能不是很有用。但是,如果您随后将 ForEach 扩展方法的实现替换为使用多线程方法,那么您将获得并行性的优势。
这显然并不总是有效,这种实现仅在循环迭代完全相互独立时才有效,但它可能很有用。
另外:永远要警惕那些说某些编程结构总是错误的人。
Your colleague is wrong about for loops being bad in all cases, but correct that they can be rewritten functionally.
Say you have an extension method that looks like this:
Then you can write a loop like this:
This may not be very useful now. But if you then replace your implementation of the ForEach extension method for use a multi threaded approach then you gain the advantages of parallelism.
This obviously isn't always going to work, this implementation only works if the loop iterations are completely independent of each other, but it can be useful.
Also: always be wary of people who say some programming construct is always wrong.
一个简单的(实际上毫无意义的)示例:
Loop
Linq
在我的书中,第二个看起来更整洁和简单,尽管第一个没有任何问题。
A simple (and pointless really) example:
Loop
Linq
In my book, the second one looks a lot tidier and simpler, though there's nothing wrong with the first one.
有时,如果存在更有效的替代方案,则 for 循环会很糟糕。例如搜索,对列表进行排序然后使用快速排序或二进制排序可能会更有效。或者当您迭代数据库中的项目时。在数据库中使用基于集合的操作通常比迭代项目更有效。
否则,如果 for 循环,尤其是 for-each 最有意义并且可读,那么我会选择它,而不是将其分解为不那么直观的东西。我个人不相信这些听起来很宗教的“总是这样做,因为这是唯一的方法”。相反,最好有指导方针,并了解在什么情况下适合应用这些指导方针。你问“为什么”是件好事!
Sometimes a for-loop is bad if there exists a more efficient alternative. Such as searching, where it might be more efficient to sort a list and then use quicksort or binary sort. Or when you are iterating over items in a database. It is usually much more efficient to use set-based operations in a database instead of iterating over the items.
Otherwise if the for-loop, especially a for-each makes the most sense and is readable, then I would go with that rather than rafactor it into something that isn't as intuitive. I personally don't believe in these religious sounding "always do it this way, because that is the only way". Rather it is better to have guidelines, and understand in what scenarios it is appropriate to apply those guidelines. It is good that you ask the Why's!
可以说,For 循环是“糟糕的”,因为它意味着 CPU 中的分支预测,并且当分支预测失败时,性能可能会下降。
但是 CPU(分支预测准确度为 97%)和具有循环展开等技术的编译器,使得循环性能的降低可以忽略不计。
For loop is, let's say, "bad" as it implies branch prediction in CPU, and possibly performance decrease when branch prediction miss.
But CPU (having a branch prediction accuracy of 97%) and compiler with tecniques like loop unrolling, make loop performance reduction negligible.
如果直接抽象
for
循环,您会得到:从函数式编程的角度来看,我遇到的问题是
void
返回类型。它本质上意味着for
循环不能很好地与任何东西组合。因此,我们的目标不是从for
循环到某个函数的 1-1 转换,而是从功能角度思考并避免做不组合的事情。不要考虑循环和行动,而是考虑整个问题以及您要映射的内容和映射到的内容。If you abstract the
for
loop directly you get:The problem I have with this from a functional programming perspective is the
void
return type. It essentially means thatfor
loops do not compose nicely with anything. So the goal is not to have a 1-1 conversion fromfor
loop to some function, it is to think functionally and avoid doing things that do not compose. Instead of thinking of looping and acting think of the whole problem and what you are mapping from and to.for 循环始终可以替换为不涉及使用循环的递归函数。递归函数是一种更具功能性的编程方式。
但如果你盲目地用递归函数代替 for 循环,那么小猫和小狗都会死亡数以百万计,你也会被迅猛龙杀死。
好的,这是一个例子。但请记住,我不主张进行此更改!
for 循环
可以更改为递归函数调用
这对于大多数值来说应该是一样的,但它不太清晰,效率较低,并且如果数组太大,可能会耗尽堆栈。
A for loop can always be replaced by a recursive function that doesn't involve the use of a loop. A recursive function is a more functional stye of programming.
But if you blindly replace for loops with recursive functions, then kittens and puppies will both die by the millions, and you will be done in by a velocirapter.
OK, here's an example. But please keep in mind that I do not advocate making this change!
The for loop
Can be changed to this recursive function call
This should work just the same for most values, but it is far less clear, less effecient, and could exhaust the stack if the array is too large.
您的同事可能会建议,在涉及数据库数据的某些情况下,最好在查询时使用聚合 SQL 函数,例如 Average() 或 Sum(),而不是在 ADO .NET 中在 C# 端处理数据。应用。
否则,如果使用得当,for 循环会非常有效,但要意识到,如果您发现自己将它们嵌套到三个或更多顺序,则可能需要一种更好的算法,例如涉及递归、子例程或两者的算法。例如,冒泡排序在最坏情况(逆序)情况下的运行时间为 O(n^2),但递归排序算法仅为 O(n log n),这要好得多。
希望这有帮助。
Your colleague may be suggesting under certain circumstances where database data is involved that it is better to use an aggregate SQL function such as Average() or Sum() at query time as opposed to processing the data on the C# side within an ADO .NET application.
Otherwise for loops are highly effective when used properly, but realize that if you find yourself nesting them to three or more orders, you might need a better algorithm, such as one that involves recursion, subroutines or both. For example, a bubble sort has a O(n^2) runtime on its worst-case (reverse order) scenario, but a recursive sort algorithm is only O(n log n), which is much better.
Hopefully this helps.
任何语言中的任何构造都是有原因的。它是用来完成任务的工具。意味着结束。在每种情况下,都有适当使用它的方式,即以清晰简洁的方式并在语言的精神范围内和滥用它的方式。这适用于严重错位的
goto
语句以及for
循环难题,以及while
、do-while
、switch/case
、if-then-else
等。如果for
循环是适合您的工具在做时,使用它,你的同事将需要接受你的设计决策。Any construct in any language is there for a reason. It's a tool to be used to accomplish a task. Means to an end. In every case, there are manners in which to use it appropriately, that is, in a clear and concise way and within the spirit of the language AND manners to abuse it. This applies to the much-misaligned
goto
statement as well as to yourfor
loop conundrum, as well aswhile
,do-while
,switch/case
,if-then-else
, etc. If thefor
loop is the right tool for what you're doing, USE IT and your colleague will need to come to terms with your design decision.这取决于循环中的内容,但他/她可能指的是递归函数
It depends upon what is in the loop but he/she may be referring to a recursive function
问题是循环是否会改变状态或引起副作用。如果是这样,请使用 foreach 循环。如果没有,请考虑使用 LINQ 或其他函数结构。
请参阅“foreach”与“ForEach” 在 Eric Lippert 的博客上。
The question is if the loop will be mutating state or causing side effects. If so, use a foreach loop. If not, consider using LINQ or other functional constructs.
See "foreach" vs "ForEach" on Eric Lippert's Blog.