如何用C#中的函数语句替换for循环?

Ler*_*rve 32 c# linq lambda for-loop functional-programming

一位同事曾经说过,每当我写一个for-loop时,上帝就会杀死一只小猫.

当被问及如何避免for循环时,他的回答是使用函数式语言.但是,如果你坚持使用非功能性语言,比如说C#,有什么技术可以避免for循环或通过重构来摆脱它们?有了lambda表达式和LINQ吗?如果是这样,怎么样?

问题

所以问题归结为:

  1. 为什么for循环不好?或者,在什么情况下for循环要避免,为什么?
  2. 你能提供C#代码示例,看看它之前的样子,即循环,然后没有循环吗?

dtb*_*dtb 30

在您操作某些数据集并希望转换,过滤或聚合元素的情况下,功能构造通常比for循环更清楚地表达您的意图.

当您想要重复执行某些操作时,循环非常合适.


例如

int x = array.Sum();
Run Code Online (Sandbox Code Playgroud)

更明确地表达了你的意图

int x = 0;
for (int i = 0; i < array.Length; i++)
{
    x += array[i];
}
Run Code Online (Sandbox Code Playgroud)

  • @Dan Tao:你好*可以*使用for循环实现`Sum`,或者你可以使用递归来实现;)当然,如果你专注于实现细节,你会真的错过这一点.For循环是一种反模式,因为它们不像高阶函数那样可组合. (12认同)
  • 是的,但您如何期望实施"Sum"?这实际上是使用方法的一个论据,而不是完全避免使用`for`循环. (8认同)
  • @Juliet:如果你专注于实现细节,可能是因为你正在实现一些东西.我们不仅仅是预先存在的库的消费者,而是将所有实现细节从我们中抽象出来; 我们还创建我们自己的*抽象,并实现我们自己的*功能,我们有时使用`for`循环(*或*递归 - 虽然我会严重质疑任何使用递归实现`Sum`方法的人的动机!)这样做.对我来说,称为"for"循环是一种"反模式",就像是说我们应该抛弃指甲,因为我们无法建立城市. (6认同)
  • @DanTao:在考虑正确使用尾递归时,使用递归实现求和与使用迭代方法没有什么不同.Sum只是一个更通用的序列扫描的特例,可以有效地实现.你不需要一个函数式语言,它可以在C#中实现就好了(当然使用功能习语). (3认同)

Jul*_*iet 16

为什么for循环不好?或者,在什么情况下for循环要避免,为什么?

如果你的同事有函数式编程,那么他可能已经熟悉了避免for循环的基本原因:

折叠/映射/过滤器涵盖了列表遍历的大多数用例,并且非常适合函数组合.For循环不是一个好的模式,因为它们不可组合.

大多数情况下,您遍历列表以折叠(聚合),映射或过滤列表中的值.这些高阶函数已经存在于每种主流函数语言中,因此您很少看到函数代码中使用的for循环习语.

高阶函数是函数组合的基础,这意味着您可以轻松地将简单函数组合成更复杂的函数.

为了给出一个非常重要的例子,请用命令式语言考虑以下内容:

let x = someList;
y = []
for x' in x
    y.Add(f x')

z = []
for y' in y
    z.Add(g y')
Run Code Online (Sandbox Code Playgroud)

在函数式语言中,我们编写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)的方式,可以让你换出mapfilterfold需要:

let apply2 a g b f x = a g (b f x)
Run Code Online (Sandbox Code Playgroud)

并使用称它为apply2 map g filter fapply2 map g map fapply2 filter g filter f或任何你需要的.现在你可能永远不会在现实世界中编写类似的代码,你可能会使用以下方法简化它:

let mapmap g f = apply2 map g map f
let mapfilter g f = apply2 map g filter f
Run Code Online (Sandbox Code Playgroud)

高阶函数和函数组合为您提供了使用命令式代码无法获得的抽象级别.

通过抽象循环的实现细节,您可以无缝地将一个循环交换为另一个循环.

请记住,for循环是一个实现细节.如果需要更改实现,则需要更改每个for循环.

映射/折叠/过滤抽象循环.因此,如果要更改循环的实现,可以在这些函数中更改它.

现在你可能想知道为什么你想抽象出一个循环.考虑将项目从一种类型映射到另一种类型的任务:通常,项目一次映射一个,按顺序映射,并独立于所有其他项目映射.大多数情况下,像这样的地图是并行化的主要候选者.

不幸的是,顺序映射和并行映射的实现细节是不可互换的.如果你的代码中有大量的顺序映射,并且你想要将它们换成并行映射,那么你有两种选择:在你的代码库中复制/粘贴相同的并行映射代码,或者将映射逻辑抽象为两个函数mappmap.一旦你走了第二条路线,你就已经在功能编程方面处于领先地位.

如果您了解函数组合的目的并抽象出实现细节(甚至细节像循环一样简单),您就可以开始了解函数式编程如何以及为何如此强大.


Ree*_*sey 14

  1. 对于循环也不错.保持for循环有许多非常有效的理由.
  2. 您可以通过在C#中使用LINQ对其进行重新处理来"避免"for循环,这提供了更具说明性的语法.根据具体情况,这可能是好是坏:

比较以下内容:

var collection = GetMyCollection();
for(int i=0;i<collection.Count;++i)
{
     if(collection[i].MyValue == someValue)
          return collection[i];
}
Run Code Online (Sandbox Code Playgroud)

vs foreach:

var collection = GetMyCollection();
foreach(var item in collection)
{
     if(item.MyValue == someValue)
          return item;
}
Run Code Online (Sandbox Code Playgroud)

与LINQ:

var collection = GetMyCollection();
return collection.FirstOrDefault(item => item.MyValue == someValue);
Run Code Online (Sandbox Code Playgroud)

就个人而言,所有三个选项都有它们的位置,我全部使用它们.这是为您的方案使用最合适的选项的问题.

  • 如果你*需要*索引,你不一定需要for循环.选择((x,i)=> ...)在许多情况下都可以正常工作. (4认同)
  • @Lernkurve:就我个人而言,我根据以下标准(按顺序)选择:正确性,可读性,可维护性.无论哪个最容易理解往往是三者中的最佳选择...(只有当你需要**索引时才需要for循环,否则,使用foreach或LINQ,IMO.) (2认同)
  • @ccomet:当它更清楚时.通常,对我来说,这意味着for循环试图"找到"元素,过滤或执行一个映射/累积操作的任何时候.如果在块内发生多个操作,则foreach通常更清晰. (2认同)
  • @ccomet,他确实提供了一个很好的例子.如果要对集合中的每个元素执行操作,则foreach循环是合适的.但是,如果您要做的只是提取符合给定条件的对象,那么LINQ是一个不错的选择.事实上,您通常会在代码后面的LINQ结果中找到foreach. (2认同)

Ian*_*cer 14

for循环没有任何问题,但是这里有一些人们可能更喜欢LINQ这样的函数/声明性方法的原因,你可以在其中声明你想要的东西,而不是你如何得到它:

  1. 功能方法可能更容易使用PLINQ或编译器手动并行化.随着CPU迁移到更多核心,这可能变得更加重要.

  2. 功能方法使得在多步骤过程中更容易实现延迟评估,因为您可以将中间结果作为一个尚未完全评估的简单变量传递到下一步,而不是完全评估第一步,然后将集合传递给下一步(或不使用单独的方法和yield语句来实现相同的程序).

  3. 功能方法通常更短,更容易阅读.

  4. 功能方法通常会消除for循环中的复杂条件体(例如if语句和'continue'语句),因为您可以将for循环分解为逻辑步骤 - 选择匹配的所有元素,对它们执行操作,...


Mua*_*Dib 8

对于循环不会杀死人(或小猫,或小狗,或tribbles).人们杀人.对于循环,它本身也不错.然而,就像其他任何事情一样,你使用它们的方式可能很糟糕.

  • For循环不会杀人,但转到肯定会.http://xkcd.com/292/ (14认同)
  • 它们可以杀死可读性,例如list.Contains("foo")vs for循环. (2认同)
  • @Petar:`for`循环是新的`goto`.:) (2认同)

小智 6

有时你不会只杀死一只小猫.

for(int i = 0; i <kittens.Length; i ++){kittens [i] .Kill(); }

有时候你会全部杀掉他们.

  • 错误的OO.为什么在`Kitten`类中名为`Kill`​​的方法会自行杀死?你应该将`kittens [i] .Kill()`改为`God.Kill(kittens [i])`.虽然+1的幽默. (5认同)

Sté*_*ane 5

您可以很好地重构代码,这样您就不会经常看到它们.一个好的函数名称肯定比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);
    }
}
Run Code Online (Sandbox Code Playgroud)

LINQ

// mystrings is a string array
List<string> myList = mystrings.Where<string>(t => t.Length > 5)
                               .ToList<string();
Run Code Online (Sandbox Code Playgroud)

当您在功能中使用第一个或第二个版本时,它更容易阅读

var filteredList = myList.GetStringLongerThan(5);
Run Code Online (Sandbox Code Playgroud)

现在这是一个非常简单的例子,但你明白我的观点.