如何用 C# 中的函数语句替换 for 循环?
Posted
技术标签:
【中文标题】如何用 C# 中的函数语句替换 for 循环?【英文标题】:How to replace for-loops with a functional statement in C#? 【发布时间】:2011-02-08 11:28:00 【问题描述】:一位同事曾经说过,每次我写一个for循环,上帝都在杀死一只小猫。
当被问及如何避免 for 循环时,他的回答是使用函数式语言。但是,如果您坚持使用非函数式语言,例如 C#,有哪些技术可以避免 for 循环或通过重构来摆脱它们?也许使用 lambda 表达式和 LINQ?如果有,怎么做?
问题
所以问题归结为:
-
为什么 for 循环不好?或者,在什么情况下要避免 for 循环以及为什么?
您能否提供 C# 代码示例,说明它之前的样子,即有循环,之后没有循环?
【问题讨论】:
For 循环还不错。你的同事误入歧途了。 他还不如告诉你,“不要写太多的 for 循环,你会失明。”这也有同样的可信度。 For 循环是最自然的迭代结构。甚至函数式语言也使用迭代(尤其是在其代码解释器内部)。 量子力学中的 for 循环会杀死薛定谔的猫。 @Loadmaster:“即使是函数式语言也使用迭代(尤其是在其代码解释器内部)。” 【参考方案1】:在您对某些数据集进行操作并希望转换、过滤或聚合元素的情况下,函数构造通常比 for 循环更清楚地表达您的意图。
当您想要重复执行某些操作时,循环非常适合。
例如
int x = array.Sum();
比
更清楚地表达了你的意图int x = 0;
for (int i = 0; i < array.Length; i++)
x += array[i];
【讨论】:
是的,但是您希望如何实现Sum
?这确实是使用方法的一个论据,而不是完全避免 for
循环。
@Dan Tao:我指的是LINQ Sum方法。当然,在某些地方必须有一个循环。但是我们可以为我们构建一组很好的方法(例如 LINQ 提供的方法),将循环抽象出来并使我们的代码更具可读性。
@Dan Tao:你可以使用for循环实现Sum
,或者你可以使用递归来实现;)当然,如果你专注于实现细节,你真的会错过重点。 For 循环是一种反模式,因为它们不像高阶函数那样可组合。
@Juliet:如果你专注于实现细节,大概是因为你正在实现一些东西。我们不都是简单的现有库的消费者,这些库将所有实现细节从我们手中抽象出来。我们还创建了我们的自己的抽象,并实现了我们的自己的功能,我们有时使用for
循环(或递归——尽管我会严重质疑任何使用递归实现Sum
方法的人的动机!)这样做。对我来说,将for
循环称为“反模式”就像是在说我们应该放弃钉子,因为我们不能用钉子建造城市。
@DanTao:在考虑正确使用尾递归时,使用递归实现 sum 与使用迭代方法没有什么不同。 Sum 只是更一般的序列扫描的一个特例,可以有效地实现。你不需要函数式语言,它可以用 C# 实现就好(当然使用函数式习语)。【参考方案2】:
为什么 for 循环不好?或者,在什么 上下文是要避免的 for 循环和 为什么?
如果你的同事有函数式编程,那么他可能已经熟悉避免 for 循环的基本原因:
Fold / Map / Filter 涵盖了列表遍历的大多数用例,并且非常适合函数组合。 For 循环不是一个好的模式,因为它们不可组合。
大多数时候,您遍历列表以折叠(聚合)、映射或过滤列表中的值。这些高阶函数已经存在于每一种主流函数式语言中,所以你很少在函数式代码中看到 for-loop 习惯用法。
高阶函数是函数组合的基础,这意味着您可以轻松地将简单函数组合成更复杂的函数。
举一个重要的例子,用命令式语言考虑以下内容:
let x = someList;
y = []
for x' in x
y.Add(f x')
z = []
for y' in y
z.Add(g y')
在函数式语言中,我们会写成map g (map f x)
,或者我们可以使用map (f . g) x
消除中间列表。现在,原则上我们可以从命令式版本中删除中间列表,这会有所帮助——但不会太大。
命令式版本的主要问题在于 for 循环是实现细节。如果你想改变函数,你就改变它的实现——你最终会修改很多代码。
举个例子,你会如何在命令式中写map g (filter f x)
?好吧,由于您不能重用映射和映射的原始代码,因此您需要编写一个新函数来代替过滤和映射。如果你有 50 种映射方式和 50 种过滤方式,你需要 50^50 个函数,或者你需要使用命令模式模拟将函数作为一等参数传递的能力(如果你曾经尝试过函数式编程在 Java 中,你明白这可能是一场噩梦)。
回到函数世界,您可以概括 map g (map f x)
,以便根据需要将 map
替换为 filter
或 fold
:
let apply2 a g b f x = a g (b f x)
并使用apply2 map g filter f
或apply2 map g map f
或apply2 filter g filter f
或任何您需要的方式调用它。现在你可能永远不会在现实世界中编写这样的代码,你可能会使用以下方法来简化它:
let mapmap g f = apply2 map g map f
let mapfilter g f = apply2 map g filter f
高阶函数和函数组合为您提供了使用命令式代码无法获得的抽象级别。
抽象出循环的实现细节,让您可以无缝地将一个循环换成另一个循环。
请记住,for 循环是一个实现细节。如果需要更改实现,则需要更改每个 for 循环。
映射/折叠/过滤抽象掉循环。因此,如果您想更改循环的实现,请在这些函数中进行更改。
现在您可能想知道为什么要抽象出一个循环。考虑将项目从一种类型映射到另一种类型的任务:通常,项目一次映射一个,顺序地,并且独立于所有其他项目。大多数时候,像这样的地图是并行化的主要候选者。
不幸的是,顺序映射和并行映射的实现细节是不可互换的。如果您的代码中有大量顺序映射,并且您希望将它们换成并行映射,那么您有两种选择:在整个代码库中复制/粘贴相同的并行映射代码,或者将映射逻辑抽象为两个函数map
和 pmap
。一旦你走第二条路,你就已经深入到函数式编程领域了。
如果您了解函数组合的目的并抽象出实现细节(甚至是像循环这样微不足道的细节),您就可以开始理解函数式编程如何以及为何如此强大。
【讨论】:
哦,在一些小丑说“但不是用 for 循环实现的映射、折叠和过滤器!”之前。答:不总是。它们很容易通过递归实现:let fold f seed = function [] -> seed | x::xs -> fold f (f seed x) xs
、let map f = [] -> [] | x::xs -> f x::map f xs
和 let filter f = [] -> [] | x::xs -> if f x then x::filter f xs or filter f xs
。
早在我滚动到足够远的地方进行确认之前,我就知道这是朱丽叶的帖子。说得好。
很好的答案,不可否认。但是回复:您对递归的评论:仅仅因为使用递归可以像使用for
循环一样容易地实现某些功能,并不意味着(对我来说,无论如何)它是最好的——在实现的上下文中这样的功能——避免for
循环。这是在乞求问题,真的。对我来说,这就像在说,“你不应该吃苹果。” “为什么不?” “因为你可以吃橙子。”
抱歉,-1 因为没有回答“如何做”的问题,也没有按要求使用 C#。【参考方案3】:
-
For 循环还不错。保留 for 循环有很多非常正当的理由。
您通常可以通过使用 C# 中的 LINQ 重新设计 for 循环来“避免”循环,它提供了更具声明性的语法。根据具体情况,这可能是好是坏:
比较以下:
var collection = GetMyCollection();
for(int i=0;i<collection.Count;++i)
if(collection[i].MyValue == someValue)
return collection[i];
vs foreach:
var collection = GetMyCollection();
foreach(var item in collection)
if(item.MyValue == someValue)
return item;
对比林克:
var collection = GetMyCollection();
return collection.FirstOrDefault(item => item.MyValue == someValue);
就我个人而言,这三个选项都有自己的位置,我都会使用它们。这是为您的场景使用最合适的选项的问题。
【讨论】:
@Lernkurve:就个人而言,我根据以下标准(按顺序)进行选择:正确性、可读性、可维护性。最容易理解的往往是三者中的最佳选择......(只有在需要索引时才真正需要 For 循环,否则,请使用 foreach 或 LINQ,IMO。)跨度> 如果你需要你不一定需要一个for循环的索引。 Select((x,i) => ...) 在很多情况下都可以正常工作。 @ccomet:当它更清楚时。通常,对我来说,这意味着任何时候 for 循环都试图“找到”一个元素、过滤器或执行一个映射/累积操作。如果块内发生多个操作,foreach 通常更干净。 @ccomet,上面他确实提供了一个很好的例子。如果要对集合中的每个元素执行操作,则适合使用 foreach 循环。但是,如果您只想提取与给定条件匹配的对象,那么 LINQ 是一个不错的选择。事实上,您通常会在代码后面的 LINQ 结果中发现 foreach 发生。 这些天我主要使用函数式语法并在以下情况下回退到 for 循环:- A. 循环体包含复杂的逻辑,无法解开成更清晰的函数顺序应用程序,而且更简单只需编写一个包含复杂条件代码的 for 循环。 B. 任务本质上没有功能,即有副作用 C. 任务需要在其中处理异常。当然,您可以编写带有 try catch 的大 lambda 块,但在某些时候,使用 for 循环会变得更容易和更清晰。【参考方案4】:for 循环没有任何问题,但人们可能更喜欢函数式/声明式方法(如 LINQ)的一些原因,在这种方法中你声明你想要什么而不是你如何获得它:-
函数式方法可能更容易使用 PLINQ 手动或通过编译器进行并行化。随着 CPU 转向更多内核,这可能变得更加重要。
函数式方法可以更轻松地在多步过程中实现惰性求值,因为您可以将中间结果作为尚未完全求值的简单变量传递到下一步,而不是完全求值第一步然后将集合传递到下一步(或不使用单独的方法和 yield 语句在程序上实现相同的目标)。
函数式方法通常更短且更易于阅读。
函数式方法通常会消除 for 循环中的复杂条件体(例如 if 语句和 'continue' 语句),因为您可以将 for 循环分解为逻辑步骤 - 选择所有匹配的元素,对它们执行操作, ...
【讨论】:
【参考方案5】:For 循环不会杀死人(或小猫、小狗或 tribbles)。人杀人。 for 循环本身并不坏。但是,就像其他任何事情一样,您使用它们的方式可能会很糟糕。
【讨论】:
@Muad 他说的是杀死小猫,而不是人。你跑题了,j/k +1 但是人们会杀死小猫吗?也许只有 Harkonnens。 哈哈,真的,我是在转述关于枪支的流行说法。 它们可以杀死可读性,例如 list.Contains("foo") 与 for 循环。 @Petar:for
循环是新的goto
。 :)【参考方案6】:
有时你不会只杀死一只小猫。
for (int i = 0; i
有时你把他们都杀了。
【讨论】:
错误的OO。为什么Kitten
类中名为 Kill
的方法会杀死自己?您应该将kittens[i].Kill()
更改为God.Kill(kittens[i])
。为幽默 +1。【参考方案7】:
您可以很好地重构您的代码,这样您就不会经常看到它们。一个好的函数名肯定比 for 循环更具可读性。
以 AndyC 为例:
循环
// mystrings is a string array
List<string> myList = new List<string>();
foreach(string s in mystrings)
if(s.Length > 5)
myList.add(s);
灵巧
// mystrings is a string array
List<string> myList = mystrings.Where<string>(t => t.Length > 5)
.ToList<string();
无论您在函数中使用第一个版本还是第二个版本,都更易于阅读
var filteredList = myList.GetStringLongerThan(5);
现在这是一个过于简单的例子,但你明白我的意思。
【讨论】:
【参考方案8】:你的同事不对。 For 循环本身并不坏。它们干净、易读且不易出错。
【讨论】:
【参考方案9】:你的同事认为 for 循环在所有情况下都不好,但正确的是它们可以在功能上重写。
假设您有一个如下所示的扩展方法:
void ForEach<T>(this IEnumerable<T> collection, Action <T> action)
foreach(T item in collection)
action(item)
然后你可以这样写一个循环:
mycollection.ForEach(x => x.DoStuff());
现在这可能不是很有用。但是,如果您随后将 ForEach 扩展方法的实现替换为使用多线程方法,那么您将获得并行性的优势。
这显然并不总是有效,这个实现只有在循环迭代完全相互独立的情况下才有效,但它可能很有用。
另外:总是要警惕那些说某些编程结构总是错误的人。
【讨论】:
【参考方案10】:一个简单(实际上毫无意义)的例子:
循环
// mystrings is a string array
List<string> myList = new List<string>();
foreach(string s in mystrings)
if(s.Length > 5)
myList.add(s);
Linq
// mystrings is a string array
List<string> myList = mystrings.Where<string>(t => t.Length > 5).ToList<string>();
在我的书中,第二个看起来更整洁、更简单,尽管第一个并没有什么问题。
【讨论】:
Where<string>
注释以获得更简洁的代码 - 编译器足够聪明,可以理解您的意思。【参考方案11】:
有时,如果存在更有效的替代方案,则 for 循环是不好的。例如搜索,对列表进行排序然后使用快速排序或二进制排序可能更有效。或者当您迭代数据库中的项目时。在数据库中使用基于集合的操作通常比迭代项目更有效。
否则,如果 for 循环,尤其是 for-each 最有意义且可读性最强,那么我会使用它,而不是将其分解为不那么直观的东西。我个人不相信这些宗教听起来“总是这样做,因为这是唯一的方式”。相反,最好有指导方针,并了解在什么情况下应用这些指导方针是合适的。你问为什么很好!
【讨论】:
【参考方案12】:比方说,For 循环是“坏的”,因为它意味着 CPU 中的分支预测,并且在分支预测未命中时可能会降低性能。
但 CPU(具有 97% 的分支预测准确度)和具有循环展开等技术的编译器使得循环性能降低可以忽略不计。
【讨论】:
由于大多数 CPU 没有对整组数据进行操作的指令,因此功能代码通常会变成循环,因此很少(如果有的话)效率提高。 你是对的。另外,我想澄清一下 LINQ 不起作用,它无论如何都使用循环。【参考方案13】:如果你直接抽象 for
循环,你会得到:
void For<T>(T initial, Func<T,bool> whilePredicate, Func<T,T> step, Action<T> action)
for (T t = initial; whilePredicate(t); step(t))
action(t);
从函数式编程的角度来看,我遇到的问题是 void
返回类型。这实质上意味着for
循环不能很好地与任何东西组合。所以目标不是从for
循环到某个函数进行1-1 转换,而是从功能上思考,避免做不组合的事情。与其考虑循环和行动,不如考虑整个问题以及您映射的对象。
【讨论】:
天哪,我希望没有人会写出这样的高阶For
函数。请改用void Iter<T>(Action<T> f, IEnumerable<T> items) foreach(item item in items) f(item);
。
这正是我的观点。 for
没有有趣的类型签名。【参考方案14】:
for 循环总是可以被不涉及使用循环的递归函数替换。递归函数是一种更具功能性的编程风格。
但如果你一味地用递归函数替换 for 循环,那么小猫和小狗都会死于数百万,而你将被迅猛龙所杀。
好的,这是一个例子。但请记住,我不提倡进行此更改!
for 循环
for (int index = 0; index < args.Length; ++index)
Console.WriteLine(args[index]);
可以改成这个递归函数调用
WriteValuesToTheConsole(args, 0);
static void WriteValuesToTheConsole<T>(T[] values, int startingIndex)
if (startingIndex < values.Length)
Console.WriteLine(values[startingIndex]);
WriteValuesToTheConsole<T>(values, startingIndex + 1);
这对于大多数值来说应该是一样的,但它远没有那么清晰,效率较低,并且如果数组太大,可能会耗尽堆栈。
【讨论】:
谢谢。你能提供这样一个替换的代码示例吗?【参考方案15】:您的同事可能会建议在某些涉及数据库数据的情况下,最好在查询时使用聚合 SQL 函数,例如 Average() 或 Sum(),而不是在 C# 端处理数据。 ADO .NET 应用程序。
否则,如果使用得当,for 循环非常有效,但要意识到,如果您发现自己将它们嵌套到三个或更多顺序,您可能需要一种更好的算法,例如涉及递归、子例程或两者兼有的算法。例如,冒泡排序在其最坏情况(倒序)情况下的运行时间为 O(n^2),但递归排序算法只有 O(n log n),这要好得多。
希望这会有所帮助。
吉姆【讨论】:
【参考方案16】:任何语言的任何结构都是有原因的。它是用来完成任务的工具。意味着结束。在每种情况下,都有适当使用它的方式,即以清晰简洁的方式并在语言的精神范围内以及滥用它的方式。这适用于严重错位的goto
语句以及for
循环难题,以及while
、do-while
、switch/case
、if-then-else
等。如果for
循环是适合您正在做的事情的正确工具,使用它,您的同事需要接受您的设计决策。
【讨论】:
【参考方案17】:这取决于循环中的内容,但他/她可能指的是递归函数
//this is the recursive function
public static void getDirsFiles(DirectoryInfo d)
//create an array of files using FileInfo object
FileInfo [] files;
//get all files for the current directory
files = d.GetFiles("*.*");
//iterate through the directory and print the files
foreach (FileInfo file in files)
//get details of each file using file object
String fileName = file.FullName;
String fileSize = file.Length.ToString();
String fileExtension =file.Extension;
String fileCreated = file.LastWriteTime.ToString();
io.WriteLine(fileName + " " + fileSize +
" " + fileExtension + " " + fileCreated);
//get sub-folders for the current directory
DirectoryInfo [] dirs = d.GetDirectories("*.*");
//This is the code that calls
//the getDirsFiles (calls itself recursively)
//This is also the stopping point
//(End Condition) for this recursion function
//as it loops through until
//reaches the child folder and then stops.
foreach (DirectoryInfo dir in dirs)
io.WriteLine("--------->> 0 ", dir.Name);
getDirsFiles(dir);
【讨论】:
【参考方案18】:问题是循环是否会改变状态或引起副作用。如果是这样,请使用 foreach 循环。如果没有,请考虑使用 LINQ 或其他函数式构造。
请参阅 Eric Lippert 博客上的 "foreach" vs "ForEach"。
【讨论】:
以上是关于如何用 C# 中的函数语句替换 for 循环?的主要内容,如果未能解决你的问题,请参考以下文章
如何用Pythoncontinue语句及单个for循环输出10~20之间的所有奇数及21~30间的?