1. 介绍
从理论走向实践,合理会问一个问题:“作为 .NET / C# 的开发者,我为什么要学这些函数式编程的手法?”
确实,C# 不是像 F# 或 Haskell 那样的纯函数式语言。但从 3.0 版本开始一直到 C# 14,它加入了很多函数式的工具,这些东西能显著提升代码质量和表达力。
这些场景里它们尤其好用:
- 集合处理 — LINQ, Map/Reduce, 过滤、聚合、排序等数据“魔法”。
- 纯函数 — 更少因状态和副作用引起的 bug,调试更简单。
- 高阶函数 — 写出通用、可复用的组件,用起来很爽。
- 不可变性在并发中的作用 — 在并行和异步场景中,是保证代码安全的关键之一。
- 函数组合 — 让复杂的业务逻辑变得简洁、可读、易测。
表格:C# 中 OOP 方法与函数式方法对比
| 任务 | 命令式(OOP / 老派) | 函数式(FP) |
|---|---|---|
| 列表过滤 | |
|
| 列表转换 | |
|
| 按条件查找 | |
|
| 聚合 | |
|
| 缓存 | |
|
2. LINQ:C# 最函数化的部分
如果你觉得“函数式编程”就是处理列表、过滤器和那些 .Where, .Select, .Aggregate —— 恭喜你:确实是这样!LINQ 可以说是 C# 里的函数式精华。
回顾一下 LINQ 是怎么运作的
LINQ 用方法链来操作集合,这些方法通常接受函数做参数(比如 lambda)。例如:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// 只取偶数并把它们翻倍
var result = numbers
.Where(x => x % 2 == 0)
.Select(x => x * 2);
foreach (var number in result)
Console.WriteLine(number);
这里发生了什么?
- .Where — 是个高阶函数:接受一个函数(x => x % 2 == 0)并返回另一个集合。
- .Select — 同样接受一个函数(x => x * 2)。
- 我们没有修改原始集合,而是得到一个新的结果。
这种风格易读且好扩展(可以接上 .OrderBy, .Take, .Distinct 等)。
注意! lambda 表达式是创建“临时 delegate”的便捷写法。没有对 FP 的支持,LINQ 很难成立。
示意图:集合的函数式处理
集合 --> Where(x => bool) --> Select(x => y) --> 新结果
3. 函数组合和数据处理管道
函数式编程经常用组合:把复杂操作拆成一串小函数,每个做一件事。
示例:字符串处理的流水线
有状态风格(OOP):
string s = " hello world ";
s = s.Trim();
s = s.ToUpper();
s = s + "!";
Console.WriteLine(s); // HELLO WORLD!
更“函数式”的做法——把函数串成管道:
Func<string, string> trim = x => x.Trim();
Func<string, string> upper = x => x.ToUpper();
Func<string, string> addBang = x => x + "!";
// 函数组合 — 按顺序应用它们
Func<string, string> pipeline = x => addBang(upper(trim(x)));
Console.WriteLine(pipeline(" hello world ")); // HELLO WORLD!
一个最简单的组合子:
Func<T, R> Compose<T, U, R>(Func<T, U> f, Func<U, R> g) =>
x => g(f(x));
// 现在用 Compose 来做 pipeline:
var pipeline2 = Compose(trim, upper);
pipeline2 = Compose(pipeline2, addBang);
Console.WriteLine(pipeline2(" hello again ")); // HELLO AGAIN!
4. 使用不可变性:防止 Bug
不可变性是 FP 的基石。我们不修改数据结构,而是返回新的。在并发程序里这点尤其重要。
示例:“不对的”(可变)
List<int> numbers = new List<int> { 1, 2, 3 };
numbers[0] = 42;
示例:“正确的”(函数式)
var numbers = new List<int> { 1, 2, 3 };
var newNumbers = numbers.Select((x, i) => i == 0 ? 42 : x).ToList();
在现代 C# 里有 ImmutableList<T> 这样的集合,以及来自命名空间 System.Collections.Immutable 的类型:
using System.Collections.Immutable;
var immutableNumbers = ImmutableList.Create(1, 2, 3);
var changed = immutableNumbers.SetItem(0, 42); // 返回一个新列表!
5. 高阶函数在真实场景中的应用
高阶函数是写通用组件、避免大量条件分支的好办法。
示例:通用的用户过滤器
class User
{
public string Name { get; set; }
public int Age { get; set; }
}
var users = new List<User>
{
new User { Name = "Alice", Age = 26 },
new User { Name = "Bob", Age = 17 },
new User { Name = "Charlie", Age = 35 }
};
List<User> FilterUsers(List<User> source, Predicate<User> predicate)
{
return source.Where(u => predicate(u)).ToList();
}
// 使用:
var adults = FilterUsers(users, u => u.Age >= 18);
var longNames = FilterUsers(users, u => u.Name.Length > 3);
6. 模式匹配和 switch 表达式
现代 C# 很多地方都用 pattern matching:switch 表达式常常替代繁重的 if 链。
object value = 123;
string description = value switch
{
int i when i > 100 => "大数",
string s when s.Length > 3 => "字符串很长",
null => "空值",
_ => "未知"
};
Console.WriteLine(description); // 大数
7. memoization:给函数结果做缓存
memoization 是对相同参数缓存函数结果的技术。在 C# 里自己实现它很简单。
Func<int, int> SlowFib = null; // 递归的斐波那契函数
var cache = new Dictionary<int, int>();
SlowFib = n =>
{
if (cache.ContainsKey(n))
return cache[n];
if (n <= 1)
cache[n] = n;
else
cache[n] = SlowFib(n - 1) + SlowFib(n - 2);
return cache[n];
};
Console.WriteLine(SlowFib(40)); // 飞快!
8. 柯里化和部分应用
部分应用是把函数的一部分参数固定住。在 C# 里用 lambda 写起来很方便。
Func<int, int, int> add = (a, b) => a + b;
// 固定第一个参数
Func<int, int> add10 = b => add(10, b);
Console.WriteLine(add10(5)); // 15
Console.WriteLine(add10(100)); // 110
9. 使用函数写声明式风格
命令式:
var result = new List<int>();
foreach (var n in numbers)
{
if (n > 0)
result.Add(n * n);
}
声明式:
var result = numbers
.Where(n => n > 0)
.Select(n => n * n)
.ToList();
10. 实践任务
实现一个“学生任务管理器”里的任务过滤模块。
模型:
class StudentTask
{
public string Title { get; set; }
public bool IsCompleted { get; set; }
public int Priority { get; set; }
}
初始数据:
var tasks = new List<StudentTask>
{
new StudentTask { Title = "做作业", IsCompleted = false, Priority = 2 },
new StudentTask { Title = "喝杯咖啡", IsCompleted = true, Priority = 3 },
new StudentTask { Title = "看讲座", IsCompleted = false, Priority = 1 }
};
通用过滤器:
List<StudentTask> FilterTasks(
List<StudentTask> all,
Predicate<StudentTask> predicate)
{
return all.Where(t => predicate(t)).ToList();
}
// 查找未完成且优先级 > 1 的任务
var importantTasks = FilterTasks(tasks, t => !t.IsCompleted && t.Priority > 1);
// 输出结果
foreach (var task in importantTasks)
Console.WriteLine(task.Title);
谓词组合子:
Predicate<StudentTask> IsActive = t => !t.IsCompleted;
Predicate<StudentTask> IsHighPriority = t => t.Priority > 1;
// 合并多个条件,方式 1
var specialTasks = FilterTasks(tasks, t => IsActive(t) && IsHighPriority(t));
// 方式 2:两个 predicate 的组合子函数
Predicate<StudentTask> And(Predicate<StudentTask> a, Predicate<StudentTask> b) => t => a(t) && b(t);
var specialTasks2 = FilterTasks(tasks, And(IsActive, IsHighPriority));
11. 使用函数式方法在 C# 中的注意点和常见错误
首先,记住 C# 是强类型语言。有时候必须显式指定类型,尤其是当函数返回 delegate 或复杂的 lambda 时。否则因为类型推断失败可能会报编译错误。
其次,不要把有副作用的函数传到期望纯函数的地方。修改外部变量会破坏可预测性。尽量让你的函数不要在作用域外“污染”状态。
第三,对闭包捕获外部变量要小心,尤其是在异步或多线程代码里。被 lambda 捕获但之后值会变化的变量(比如在 LINQ 中)可能导致难以发现的 bug。
最后,过度使用函数式风格可能会让不熟悉它的团队难以维护代码。只有在它能真正简化问题时才用 FP,别为了“好看”而盲用。
GO TO FULL VERSION