准备 C# 面试时,我复习到 struct,顺手查了下 C# 10 的 [[record-struct]]。结果这一查,从 record 的 [[with]] 关键字一路追到了函数式编程,最后还搞明白了链式编程和函数式编程到底是什么关系。

✦ record 和 with 表达式

record 类型有个特点:属性默认是只读的,修改时得用 with 创建新对象。

1
var user2 = user1 with { Age = 26 };

这行代码不会改变 user1,而是生成一个新对象 user2,只有 Age 不同。这叫非破坏性修改,是 [[functional-programming]] 的典型做法。

我当时就好奇了:C# 不是面向对象语言吗,怎么还搞函数式这套?

✦ 函数式编程是什么

查了资料后,我觉得 [[functional-programming]] 就是一种写代码的约定。核心几条:

[[pure-function]]:同样输入,永远同样输出,不偷偷改全局变量,不写数据库,不打印控制台。

[[immutability]]:变量创建后不改。想改?用 with 那样生成新的。

函数当参数传递:函数可以像变量一样传来传去,C# 里用 FuncAction [[delegate]] 实现。

声明式写法:告诉程序”做什么”,而不是”怎么做”。LINQ 就是典型,比 for 循环清晰。

关于 [[pure-function]],我想起以前常用的一个技巧:用异常过滤器记日志。

1
2
try { ... }
catch(Exception ex) when (LogException(ex)) { }

这不算函数式。when 本意是条件判断,拿来偷偷执行 I/O 操作,违背了纯函数的原则。函数式处理错误一般不抛异常,而是返回 [[result-t]] 这样的包装对象。

✦ 不用语法糖也能函数式

没有 record 语法糖,能不能写函数式代码?能。

[[functional-programming]] 是思想约束。老版本 C# 里,你可以:

  • 把 class 属性设为只读,只通过构造函数赋值,手写克隆方法返回新对象。这就是 [[immutability]]。
  • 写静态方法,不碰实例状态,只靠参数算结果。这就是 [[pure-function]]。
  • 写方法接收 Func<T, bool> 参数来过滤集合。这就是高阶函数。

C# 近几年加的语法糖(LINQ、record、[[pattern-matching]]),就是让函数式代码写起来不那么别扭。

✦ 链式编程和函数式编程

看这段代码时,我脑子里冒出个问题:

1
var finalUser = user1.With(age: 26).With(name: "Alice");

这叫 [[chaining]] 还是 [[functional-programming]]?

理清后发现,这是两回事:

[[chaining]] 是写法:方法返回对象,就能用点号接下去。

[[functional-programming]] 是约束:链的每一步都不能改原数据,得生成新数据。

StringBuilder 就是反例:

1
sb.Append("Hello").Append("World");

这是 [[chaining]],但不是函数式。Append 在内部改了 sb 自身的状态。

LINQ 和 recordWith() 不同,它们每一步都生成新拷贝,没有副作用。所以它们既是链式写法,也是函数式实现。

✦ 小结

struct 面试题出发,顺着 recordwith 的线索,我搞明白了 [[functional-programming]],也分清了 [[chaining]] 和函数式编程的区别。

现在的 C# 已经不是纯粹的面向对象语言了。微软加的这些语法糖,大概就是想让我们用面向对象建模,用函数式写逻辑。

保持好奇心,顺着一个语法点往下挖,能学到不少东西。