反对 for 行动

  • 赵劼

2009 年 7 月 13 日

话题:敏捷.NETC#语言 & 开发架构文化 & 方法

Francesco Cirillo 于不久前发起了“反对 if 行动”,受此影响,Matthew Podwysocki 也用这种方式提出了自己的声明,即“反对 for 行动”。

Matthew Poswysocki 生活在华盛顿特区,作为微软的高级咨询师,维护或参与了诸多社区活动(如 DC ALT.NET 讨论组),并致力于推广各种敏捷实践。这次他提出,在代码中应该尽量使用和构建可以进行组合的函数,而不是显式的循环语句(包括 for、foreach 和 while)。

Matthew 认为,通过循环来实现的功能往往可以分为以下三种情况:

  1. 查询(映射、过滤等等)
  2. 聚合(求和、计数等等)
  3. 进行一些有副作用(Side Effect)的操作(读取文件、发送消息等等)

Matthew 看来,使用 for 循环来处理“查询”和“聚合”时,最大的问题在于将关注点放在了如何做(How)而不是做什么(What)。他举了一个例子,“找出 100 以内所有质数”,并给出了一个实现:

var numbers = Enumerable.Range(1, 100);
var output = new List<int>();

foreach(var number in numbers)
    if(IsPrime(number)) output.Add(number);

Matthew 认为:

这里的问题在于我们很难将现有逻辑与另一个操作进行组合,因为这里的实现涉及了“怎么做”。我们应该使用.NET 2.0 以上版本中的泛型及延迟加载的特性进行函数式的构造。这样可以带来“声明式(declarative)”的感觉,而关注点就只有“做什么”了:

var primes = Enumerable.Range(1, 100)
    .Where(x => IsPrime(x));

Matthew 提出,应该尽量避免在一个循环中进行多种操作,这样会为代码的可读性和维护性带来负面影响。而在 Martin Fowler 的Refactoring 站点中,拆分循环内部逻辑的重构方式被命名为“Split Loop”。Matthew 认为较好的方式是将逻辑进行组合,例如求出“100 以内质数的数量”便可以这样实现:

var primesCount = Enumerable.Range(1, 100)
    .Where(x => IsPrime(x))
    .Count();

对于产生副作用的情况,Matthew 引用了 Eric Lippert对于“为什么 IEnumerable<T> 没有 ForEach 扩展方法”的回应。Eric 认为,引入 IEnumerable<T> 的 ForEach 扩展方法事实上带来了副作用,而违背了 IEnumerable<T> 的设计初衷。此外,ForEach 还会形成闭包,可能会造成一些难以发现的引用问题。

Matthew 并没有赞同这种说法,不过它对这个看法表示理解。他认为,如果是使用 C# 进行编程,使用 foreach 来遍历一个 IEnumerable 没有太大问题。不过在 F# 中,最好还是使用 iter 或 iteri 方法进行遍历。关于这点,他使用 F# 交互命令行进行了演示:

> let flip f y x = f x y
- [1..10]
-   |> List.map((*) 2)
-   |> List.filter(flip (%) 3 >> (=) 0)
-   |> List.iter(printfn "%d");;
6
12
18
> [1..10]
-   |> List.map((*) 2)
-   |> List.filter(flip (%) 3 >> (=) 0)
-   |> List.iteri(printfn "%d\t%d");;
0       6
1       12
2       18

社区中有人认为,这个行动的目的是希望在面向对象编程环境中融入部分函数式编程的理念。C# 3.0 引入了 Lambda 表达式,将高阶函数在.NET 中的应用切实地推广开来。同时,其他平台也在进行着类似的改变。例如最近颇受好评的 Scala 语言也引入了函数式编程特性。

你对 for 的使用有何看法,并且对函数式编程的看法如何?

敏捷.NETC#语言 & 开发架构文化 & 方法