1. Introduction
函数式编程(FP)是一种编程范式,它的基本构建块不是对象也不是过程/方法,而是数学意义上的 函数。在函数式编程里,重点是描述 “计算什么”,而不是 “怎么计算”。
你可能已经通过 lambda 表达式和 LINQ 接触过函数式的一些想法。那区别在哪儿?实践上:OOP 描述对象和它们的交互,过程式描述一系列步骤,而函数式关注于 函数的组合、把行为作为值传递、避免修改状态(immutability),并尽量消除副作用。
Why is a new paradigm even needed?
- 代码更干净、可预测、易测。
- 并发支持更简单(“没有状态——没有问题”)。
- 简洁且富有表达力(代码越少,bug 越少)。
- 高阶、易复用的抽象。
Analogy
想象餐厅接到一个订单:“做一个煎蛋”。命令式的厨师会按步骤执行:拿鸡蛋、打蛋、打发、煎。函数式的厨师会说: res = 煎蛋(鸡蛋) —— 他用的是函数,抽象掉厨房的内部状态(嗯,差不多吧)。
在 C# 里我们可以两种风格一起用。这让语言在真实项目里非常灵活且强大。
Key concepts FP
1. Higher-order functions
函数可以作为参数传递、从其他函数返回或存到变量里。你已经用 lambda 和委托做过这些。在函数式里,这种“对函数的操作”是基本功。
2. Pure functions
如果一个函数的结果仅依赖于其参数并且不修改外部任何东西(没有副作用),那它就是“纯函数”。相同参数的两个调用会返回完全相同的结果。
3. Immutability (Immutability)
数据不在原地被改:新状态就是新对象。这让思考程序更简单,也有助于并发。
4. Absence of side effects
函数不写文件、不改全局变量、不直接在屏幕上画东西——只返回结果。现实中副作用不可避免,但我们尽量把它们隔离在系统边缘。
5. Function composition
可以把函数像积木一样组装起来。例如:先过滤正数,取平方,再求和。每个操作都是独立函数,很容易组合(Where → Select → Sum)。
2. FP in C#: from theory to practice
C# 是多范式语言:它既支持 OOP、过程式,也支持强大的函数式风格(有 lambda、委托、扩展方法和 LINQ)。
Let's analyze it with our training app as an example
假设我们在做一个操作数字和字符串列表的程序。任务是用函数式风格对这些数据做各种操作。
Example 1: Using higher-order functions
// 对列表的每个元素应用一个操作
public static void ForEach<T>(List<T> items, Action<T> action)
{
foreach (var item in items)
{
action(item);
}
}
使用示例:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
ForEach(numbers, n => Console.WriteLine(n * n)); // 函数作为参数
看到没?函数可以被“当作值”存到变量里或像普通值一样传来传去——就像厨房里的苹果一样!
Example 2: Pure function
不修改程序状态且仅依赖输入的函数:
int MultiplyByTwo(int x)
{
return x * 2;
}
- 不依赖任何外部东西。
- 不修改外部任何东西。
- 对于 x = 5 永远返回 10。
对比一下会使用并修改全局变量的函数:
int total = 0;
int AddToTotal(int x)
{
total += x;
return total;
}
这就不是纯函数——结果依赖外部状态,而且它会改变这个状态。
Пример 3: Immutability данных
不修改输入数据,而是创建新的:
List<int> AddOneToEach(List<int> numbers)
{
return numbers.Select(n => n + 1).ToList();
}
原始列表完全不变。在多线程程序里这特别方便:更少的锁、更少的数据竞态。
Пример 4: Function composition
求所有偶数的平方和:
int SumOfEvenSquares(List<int> numbers)
{
return numbers
.Where(n => n % 2 == 0) // 只保留偶数
.Select(n => n * n) // 平方
.Sum(); // 求和
}
可读且声明式:每一步都是独立的操作。
3. Useful nuances
FP, LINQ и C#
LINQ 对集合来说几乎就是“实践中的函数式编程”:你用高阶函数(Where, Select 等),得到新的序列,不去修改原始集合,每次转换都是一个独立的表达。结果是 IEnumerable<T>,它描述的是 要得到什么,而不是 怎样迭代。
Analogy table
| 命令式(过程式/面向对象) | 函数式(LINQ/函数式风格) |
|---|---|
|
|
| “修改”集合 | 得到一个新集合 |
| 状态 (total += x) | 纯函数 (xs.Sum()) |
| 按“去做什么”来描述 | 按“我们想得到什么”来描述 |
FP vs ООП: two worlds — one C#
这不是互相对立的阵营。在真实的 C# 项目里它们常常混合:领域模型(domain model)用类(OOP)建模很方便,而对集合的处理、数据聚合和转换则用函数式风格的 LINQ、lambda 和扩展方法来写。
你对委托的理解直接有用:Func<T, TResult>, Predicate<T>, Action<T> —— 这些是函数式风格常用的构件。
通用的过滤函数:
List<T> Filter<T>(List<T> items, Predicate<T> predicate)
{
var result = new List<T>();
foreach (var item in items)
{
if (predicate(item))
result.Add(item);
}
return result;
}
调用示例:
var adults = Filter(people, person => person.Age >= 18);
var bigFiles = Filter(fileNames, name => name.EndsWith(".mp4") && name.Length > 10);
比起写一堆差不多的带不同条件的方法,使用一个通用函数通常更简洁。
Зачем работодателям и собеседованиям FP-разработчики?
- 函数式有助于在不启动整个系统的情况下测试小代码块。
- 逻辑更容易维护:状态越少,bug 来源越少。
- 并行和异步代码更容易写——没有全局状态,数据竞态少。
How not to be a "fanatic"?
是的,函数式很强。但 C# 并不是纯函数式语言,也并非所有场景都需要完美的纯性。在合适的地方使用局部变量和谨慎的可变操作并没有问题。关键是可读性、可预测性和可测试性。把函数式当作工具,而不是教条。
4. Common beginner mistakes
很容易写出看起来像函数式但实际上并不函数式的代码。
比如一个函数返回了新集合,但在过程中却修改了原始列表 —— 这违背了不可变性的原则,会破坏调用者的预期。
再比如:lambda 捕获并修改了外部变量。在函数式范式里这被视为副作用,会让代码行为更难预测。
C# 编译器不会拦着你:语言允许这些写法。因此在实践函数式时要注意,保证函数“自治”,不要修改外部,也不要读取外部状态,除非通过参数传入。
GO TO FULL VERSION