目录:网上冲浪指南

F# 与 C# 对比:简单的求和问题

2020/03/18

现在让我们从一个简单的“计算从 1 到 N 的平方和”问题入手,来看看真正的 F# 代码是怎样的。我们将会对比 F# 实现与 C# 实现之间的差异,先来看看 F# 的实现吧:

// 定义平方函数
let square x = x * x

// 定义求平方和函数
let sumOfSquares n =
   [1..n] |> List.map square |> List.sum

// 让我们来试试
sumOfSquares 100

这个看起来很奇怪的 |> 叫做管道运算符。它的功能是将一个表达式的输出值传递给另一个表达式作为输入。所以我们可以这样来理解 sumOfSquares 函数:

  1. 创建一个从 1 到 N 的 List(方括号可以用来创建 List 对象)。

  2. 将这个 List 通过管道传递给库函数 List.mapList.map 使用 square 函数对传入的 List 进行转换并作为返回值输出。

  3. 将返回的 List 对象通过管道传递给另一库函数 List.sum,你大概能猜到这是干啥用的吧。

  4. 函数定义的末尾并不需要 return 来指明返回值,List.sum 的返回值将被作为整个 sumOfSquares 函数的结果返回。

然后,让我们来看看一个传统的(非函数式)C# 实现是什么样的。(后面会提到基于 LINQ 的函数式实现)

public static class SumOfSquaresHelper
{
   public static int Square(int i)
   {
      return i * i;
   }

   public static int SumOfSquares(int n)
   {
      int sum = 0;
      for (int i = 1; i <= n; i++)
      {
         sum += Square(i);
      }
      return sum;
   }
}

看到这些区别了吗?

  1. F# 代码更加简短

  2. F# 代码中没有任何类型的声明

  3. 可以交互式地开发 F# 代码

让我们对这几点逐个分析。

更简短的代码

在上面的比较中,最直观的差异由代码量体现出来。C# 需要 13 行,F# 只用了 3 行(忽略注释)。C# 代码中包含有大量的“语法噪音”,例如花括号、句末的分号等。而且 C# 的函数并不能独立存在,必须被一个类(SumOfSquaresHelper)包裹起来。F# 使用空格而不是圆括号来区分参数列表,还不需要在行末添加终止符号,而且函数也可以不依赖类独立的存在。

在 F# 中,将一个完整的函数写成一行的现象非常普遍,就比如 square 函数。sumOfSquares 也可以被压缩成一行。而这种代码风格在 C# 中则被认为是不好的。

当一个函数需要多行代码的时候,F# 使用缩进来区分代码块,这样就不需要使用花括号了(就跟 Python 一样)。所以 sumOfSquares 同样也可以被写成这样:

let sumOfSquares n =
   [1..n]
   |> List.map square
   |> List.sum

这种缩进带来的唯一问题就是你必须小心的处理不同层级代码的缩进 (要学会使用游标卡尺) 。就我个人而言,这样做是值得的。

无需声明类型

另一个差异就是 C# 代码必须显式的声明所有使用到的类型。例如参数 int i 以及返回值类型 int SumOfSquares。尽管 C# 在多数场景中允许使用 var 来替代具体的类型,但是参数与返回值类型并不能被省略。

而在上面的 F# 代码中,我们没有声明任何的类型。这非常重要:F# 看起来似乎是一个没有类型的语言,但实际上 F# 的类型安全一点不比 C# 差,有些时候甚至更加严格!F# 使用了一种被称为“类型推断”(type inference)的机制来根据上下文推导值的类型。这种机制在绝大多数场景下都非常有效,极大地降低了代码的复杂度。

在上面的例子中,类型推断算法注意到我们是从一个 int List 对象开始执行的。这反过来意味着 square 函数跟 List.sum 也必须接受 int 类型参数,而且函数最终的返回值也必须是 int。你可以在交互式窗口查看到类型推断系统推导出来的每个值的类型,例如:

val square : int -> int

这句输出的意思是 square 函数接受一个 int 作为参数并且返回一个 int 值。

如果最开始的 List 是存储的 float 值的话,类型推断系统也能够推导出 square 函数接受 float 类型参数。试一试:

// 定义平方函数
let squareF x = x * x

// 定义求平方和函数
let sumOfSquaresF n =
   [1.0 .. n] |> List.map square |> List.sum // "1.0" 是一个 float 类型的值

sumOfSquaresF 100.0

F# 的类型检查非常严格!如果你想把原始的 sumOfSquares 例子中的 List 替换成 float List([1.0 .. n]),或者将 sumOfSquaresF 中的 List 换成 int List([1 .. n]),编译器都会给你抛一个无情的类型错误。

交互式开发

最后一点,F# 提供了一个交互式窗口来供你测试运行代码。C# 中你必须借助 IDE 或者第三方工具才能实现类似的功能。

举个例子,我可以在编写完成 square 函数后马上对他进行测试:

let square x = x * x

// test
let s2 = square 2
let s3 = square 3
let s4 = square 4

当我觉得它没有问题之后,我就可以继续去写接下来的代码。这种交互性鼓励采用渐增的编码方法,从而使人上瘾!

此外,很多人认为以交互方式设计代码会强制执行良好的设计规范,例如去耦和显式声明依赖,因此,适合交互式运行的代码也更容易去进行测试。反之,难以通过交互式方法运行的代码往往也很难去进行测试。

再看 C# 实现

最开始例子中的 C# 代码是使用比较传统的方式去编写的。C# 中其实含有不少函数式的特性,利用 LINQ,我们就可以将例子中的代码写的更加简短。

使用了 LINQ 的 C# 实现
public static class FunctionalSumOfSquaresHelper
{
   public static int SumOfSquares(int n)
   {
      return Enumerable.Range(1, n)
         .Select(i => i * i)
         .Sum();
   }
}

然而除了花括号以及分号带来的噪音之外,这版 C# 实现仍然需要声明参数以及返回值的类型。