1. 介紹
從理論走向實作,合乎邏輯會問自己:「為什麼我這個 .NET / C# 開發者要學這些函數式編程的技巧?」
的確,C# 並非像 F# 或 Haskell 那樣的純函數式語言。但自 3.0 版起直到 C# 14,它加入了很多 FP 的工具,能顯著提升程式碼的品質與表達力。
以下是它們特別有效的場景:
- 處理集合 — LINQ、Map/Reduce、過濾、聚合、排序與各種資料「魔法」。
- 純函數 — 因為降低了狀態與副作用,bug 更少,也更容易除錯。
- 高階函數 — 通用且可重用的元件,使用起來很舒服。
- 在多執行緒中使用不可變性 — 是在並行與非同步場景下寫安全程式碼的關鍵之一。
- 函數組合 — 讓複雜的業務邏輯變得簡潔、易讀且容易測試。
表格:C# 中的 OOP 方法與 FP 方法比較
| 任務 | 命令式 (OOP / 舊派) | 函數式 (FP) |
|---|---|---|
| 過濾列表 | |
|
| 轉換列表 | |
|
| 根據條件查找 | |
|
| 聚合 | |
|
| 快取 | |
|
2. LINQ:C# 裡最函數式的部分
如果你以為「函數式編程」只是關於列表、過濾與那些 .Where、.Select、.Aggregate —— 恭喜,你是對的!LINQ 是 C# 中 FP 的精髓。
回顧一下 LINQ 的運作方式
LINQ 透過鏈式方法操作集合,這些方法接受函數參數(例如 lambda)。例如:
var numbers = new List<int> { 1, 2, 3, 4, 5 };
// 取得只有偶數並把它們 *2
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 的方便方法。沒有 C# 對 FP 的支援,LINQ 就不可能這麼好用。
圖示:集合的函數式處理流程
集合 --> Where(x => bool) --> Select(x => y) --> 新結果
3. 函數組合與資料處理管線
FP 常用函數組合:把複雜操作拆成一串小函數,每個只做一件事。
範例:字串處理的流水線
使用可變狀態的風格 (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
不可變性(immutability)是 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. 真實場景中的高階函數
高階函數讓你寫出通用元件,而不需大量 if/else。
範例:通用的使用者過濾器
class User
{
public string Name { get; set; }
public int Age { get; set; }
}
var users = new List<User>
{
new User { Name = "瓦西婭", Age = 26 },
new User { Name = "卡佳", Age = 17 },
new User { Name = "廖沙", 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. Pattern matching 與 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; // 遞迴的 Fibonacci 函數
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. Currying 與部分應用
部分應用是固定函數的一部分參數。在 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 組合子:
Predicate<StudentTask> IsActive = t => !t.IsCompleted;
Predicate<StudentTask> IsHighPriority = t => t.Priority > 1;
// 合併多個條件,方式一
var specialTasks = FilterTasks(tasks, t => IsActive(t) && IsHighPriority(t));
// 方式二:兩個 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 時,有時需要顯式指定型別。否則可能因為型別無法推斷而編譯失敗。
其次,不要把有副作用的函數傳到期望純函數的地方。改變外部變數會破壞可預測性。盡量讓函數不要變更其外部狀態。
再者,對於 closure(捕獲外部變數)要小心,尤其是在非同步或多執行緒程式中。如果某個變數在傳到 lambda 後仍會被改變(例如在 LINQ 中),會導致難以察覺的 bug。
最後,過度使用 FP 風格可能反而讓不熟悉此風格的團隊成員難以維護程式碼。只在能真正簡化問題時使用 FP,而不是為了「好看」而使用。
GO TO FULL VERSION