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;
}
示意:改變 vs 不改變
flowchart LR
A[原始物件] --"變異"--> B[同一個物件,但內部改變]
A --"不可變性"--> C[新的物件]
4. 在真實 C# 程式中的用途
- 現代 C# 的函式庫像是 LINQ、Entity Framework 與 ASP.NET Core 都偏好純函數與不可變性。
- 不可變性降低了那些「神祕」錯誤的數量,像是某人哪裡把重要值覆蓋掉。
- 純函數讓單元測試變得簡單:測試只需關心輸入與輸出,不用操心外部世界。
範例:處理字串
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 是會就地修改物件的。
陰險的範例:
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