1. 介绍
纯函数——是指在相同输入下总是返回相同结果且不产生任何副作用的函数。简单说:纯函数不“搞乱”周围的东西,也不被外界污染。
函数被认为是纯的,如果:
- 它只基于输入参数计算值。
- 不修改外部变量或程序状态。
- 不依赖于可能变化的外部变量或状态。
纯性的两个黄金规则
- 确定性:相同输入——相同输出。
- 无副作用:函数不改变自身外部的任何东西:既不改文件,也不改全局变量,也不改用户界面(嗨,Console.WriteLine)。
不是宗教,而是常识
看上去这可能像学术上的玩意儿,但实践会证明它很实用:
- 纯函数是可预测的。它容易测试——给定参数就能得到确定结果。
- 纯函数可以随意在代码里重排,不用担心某处会被搞坏。
- 在多核环境下,纯函数提供安全保证:可以并行运行而不用担心数据竞争。
2. 纯函数和非纯函数的示例
纯函数:一切可预测
// 纯函数:不碰外部任何东西
int Add(int a, int b)
{
return a + b;
}
int Square(int x)
{
return x * x;
}
多次调用 Add(2, 3)——每次都会是 5。虽然无聊,但可靠。
非纯函数:违反规则
// 违反纯性:依赖外部状态(静态/全局变量)
int counter = 0;
int Increase()
{
counter++;
return counter;
}
这里调用 Increase() 每次返回不同的值——不再是确定性的。
// 违反纯性:产生外部副作用(在屏幕上打印)
int AddAndPrint(int a, int b)
{
int sum = a + b;
Console.WriteLine(sum); // 副作用!
return sum;
}
那随机性和时间怎么办?
任何使用 DateTime.Now 或 Random 的函数都不是纯的:
// 非纯!
int GetRandomNumber()
{
return new Random().Next();
}
差异表
| 特征 | 纯函数 | 非纯函数 |
|---|---|---|
| 对相同参数总是相同结果 | 是 | 否 |
| 副作用 | 没有 | 有 |
| 依赖外部状态 | 没有 | 有 |
3. 数据不可变性:理论与实践
不可变性 (immutability)——是一种在创建后对象不能被修改的做法。需要新值时就创建新对象。
为什么重要?
- 应用更不容易因为意外的数据修改而崩溃。
- 没有偷偷修改的“泄露”:如果你有一个对象,别人不能在背后改它的字段。
- 不可变性是许多自动优化和并行计算的基础。
C# 中的简单例子
.NET 中的不可变类型
在 C# 中字符串(string)是不可变的!每次你做 string.Concat(s, "world"),都会创建一个新字符串。
string s = "Hello";
string t = s;
s = s + " World";
Console.WriteLine(t); // t == "Hello"
数组和集合:默认是可变的
int[] numbers = { 1, 2, 3 };
numbers[0] = 42; // 数组被修改了!
用通俗的方式理解不可变性:编译代码的思路
我们不要修改已有对象/值,而是返回新的:
// 比如不要这样做:
void AddToList(List<int> list, int value)
{
list.Add(value); // 会变异!
}
// 更好这样:
List<int> AddToList(List<int> list, int value)
{
var newList = new List<int>(list) { value }; // 新列表
return newList;
}
示意:改或不改
flowchart LR
A[原始对象] --"变异"--> B[同一对象,但内部不同]
A --"不可变性"--> C[新对象]
4. 在真实的 C# 代码里为什么需要这点?
- 在现代 C# 中,像 LINQ、Entity Framework 和 ASP.NET Core 那样的库倾向于使用纯函数和不可变性。
- 不可变性减少了那些“魔法” bug,当有人在某处把重要值覆盖掉时就不会莫名其妙出错。
- 纯函数使得自动化测试(unit tests)更简单,因为测试只需关心输入和输出,不用操心外部世界。
例子:字符串处理
string s = "Hello";
string newS = s.Replace("H", "J"); // s 仍然是 "Hello"; newS 是 "Jello"
例子:LINQ 和集合
Where, Select 等方法返回新的集合,不会改动旧的。
var numbers = new List<int> { 1, 2, 3, 4 };
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
// numbers 保持不变!
真实场景例子:通过不可变对象做配置
很多现代 .NET API 使用不可变对象来做配置,比如 JsonSerializerOptions:
var options = new JsonSerializerOptions
{
WriteIndented = true
};
// 这个对象不会在“演出”过程中被随意改变,提升了可靠性。
5. 常见错误和陷阱
问题通常出现在你在看似“纯”的代码里不小心去变异数据时。
集合经常出事:你想过滤列表,但过程中改了原始列表。
或者忘了像 List<T>.Add 这样的 API 会就地修改对象。
狡猾的例子:
List<int> DoubleTheNumbers(List<int> xs)
{
// 错误!我们变异了原始列表,返回了同一个对象。
foreach (var i in xs)
xs.Add(i * 2);
return xs;
}
这段代码甚至会抛出运行时错误(InvalidOperationException),因为我们在遍历时修改集合——这是变异导致的经典问题。
正确做法:
List<int> DoubleTheNumbers(List<int> xs)
{
var newList = new List<int>(xs.Select(x => x * 2));
return newList;
}
GO TO FULL VERSION