1. 介紹
你大概在學校聽過:「這是從函數到函數的函數」。高階函數 (Higher-Order Functions, HOF) 是指那些要麼接受函數作為參數、要麼返回函數作為結果、或者兩者都做的函數。
簡單說:如果你的方法可以把另一個函數(例如 delegate 或 lambda)當成參數接收,或者把它作為返回值給出——恭喜,你有一個高階函數!
高階函數就像一把萬用鑰匙,不但能自己開門,還能把另一把鑰匙交給你,讓你之後去開別的門。
在真實開發中的應用
聽起來有點抽象,但為什麼 C# 開發者在日常會需要這些「花招」?最簡短的答案是:
高階函數能讓程式碼更靈活、可重用且簡潔。
它們是像 LINQ、集合篩選與排序、建立資料處理管線、設定 callback、事件處理甚至依賴注入這類東西的基礎。
幾個真實場景:
- 透過把邏輯(例如篩選、排序、轉換)傳入方法,來限制方法行為。
- 寫一個通用的資料處理器,實際的操作用 delegate「塞進去」。
- 建立處理鏈(「管線」),每個函數按自己的配方改造資料。
- 做跨平台抽象:Windows 怎麼做、Linux 怎麼做?只要傳入不同的函數就行了。
2. 簡單的高階函數範例
接受另一個函數的函數
最經典的範例——方法接受 delegate 或 lambda。
// 高階函數:接受名為 process 的函數作為參數
void ForEach<T>(IEnumerable<T> collection, Action<T> process)
{
foreach (var item in collection)
{
process(item); // 呼叫作為參數的函數
}
}
// 使用範例:
var numbers = new List<int> { 1, 2, 3 };
ForEach(numbers, n => Console.WriteLine($"元素:{n}"));
這裡發生了什麼?方法 ForEach 並不知道要對每個元素做什麼。它唯一做的事就是呼叫傳進來的處理器(process)。這個處理器可以是任何東西——印到螢幕、存到資料庫、畫到 UI 上等等。
對,這就是集合的 ForEach 工作方式,而幾乎所有的 LINQ 方法本質上也是高階函數!
返回函數的函數
來個稍複雜的:方法不只接受函數,也返回函數。
// 高階函數:返回另一個函數
Func<int, int> CreateMultiplier(int factor)
{
// 返回一個使用外部變數 factor 的 lambda
return x => x * factor;
}
// 使用範例:
var multiplyBy10 = CreateMultiplier(10);
Console.WriteLine(multiplyBy10(7)); // 70
這裡 CreateMultiplier 回傳一個函數,該函數會把輸入乘上事先給定的因子。這不只是 HOF,還是個「函數工廠」的例子。
同時接受並返回函數的函數
Func<int, int> Compose(Func<int, int> f, Func<int, int> g)
{
// 回傳一個先執行 g 再執行 f 的函數:f(g(x))
return x => f(g(x));
}
// 使用範例:
Func<int, int> increment = x => x + 1;
Func<int, int> doubleIt = x => x * 2;
var incrementThenDouble = Compose(doubleIt, increment);
Console.WriteLine(incrementThenDouble(5)); // (5 + 1) * 2 = 12
正是這種組合構成了串流式資料處理的基底——比如當你連續呼叫 Select, Where, OrderBy 等方法時。
3. C# 如何支援高階函數
在純函式語言(Haskell、F#)中,函數預設就是高階的。但 C#(從 2.0 起)也因為 delegate 和 lambda 支援了這種風格。
- Delegates(Func, Action, Predicate)——函數的型別。
- Lambda 表達式——在原地建立函數的語法。
- 方法可以接受與返回 delegate——所以高階函數「開箱即用」。
視覺化示意
flowchart LR
A[資料] --> B[函數 1]
B --> C[函數 2]
C --> D[結果]
subgraph "處理管線(高階函數)"
B
C
end
4. 擴展我們的應用程式
繼續擴展我們的逐步示範應用——假設是一個「迷你字串處理器」。
新增一個簡單的高階方法
假設我們有一個使用者名稱清單,想用不同的函數隨意改造它們。
// 方法:接受字串清單和一個用來轉換的函數
List<string> TransformNames(List<string> names, Func<string, string> transformer)
{
var result = new List<string>();
foreach (var name in names)
{
result.Add(transformer(name));
}
return result;
}
怎麼用這個方法?
var names = new List<string> { "安娜", "波里斯", "謝爾蓋" };
// 轉成大寫
var upperNames = TransformNames(names, n => n.ToUpper());
// 在每個名字前加上「尊稱」
var politeNames = TransformNames(names, n => "尊敬的(先生/小姐) " + n);
foreach (var n in upperNames)
Console.WriteLine(n); // 安娜, 波里斯, 謝爾蓋
foreach (var n in politeNames)
Console.WriteLine(n); // 尊敬的(先生/小姐) 安娜, ...
方法 TransformNames 很通用:它把轉換的邏輯委託給傳進來的參數(transformer),例如呼叫 ToUpper 或任意其他規則。
適配不同型別
這個範例很容易改造成任何資料型別通用的版本。
// 泛型通用方法 - 高階函數,對任意 T 運作
List<TResult> Map<T, TResult>(List<T> items, Func<T, TResult> transformer)
{
var result = new List<TResult>();
foreach (var item in items)
{
result.Add(transformer(item));
}
return result;
}
應用示例:
var numbers = new List<int> { 1, 2, 3 };
var doubled = Map(numbers, x => x * 2); // [2, 4, 6]
var strings = Map(numbers, x => $"數字:{x}"); // ["數字:1", ...]
5. 透過高階函數做篩選與聚合
篩選與搜尋的邏輯早就透過高階函數來實作了。
// 篩選:高階函數
List<T> Filter<T>(List<T> items, Predicate<T> criteria)
{
var result = new List<T>();
foreach (var item in items)
{
if (criteria(item)) // 呼叫判斷函數
{
result.Add(item);
}
}
return result;
}
怎麼用:
var names = new List<string> { "安娜", "波里斯", "安德烈" };
var aNames = Filter(names, n => n.StartsWith("安"));
// 結果: "安娜", "安德烈"
6. 函數組合的概念 (function composition)
高階函數不只讓你用單一函數,也能把函數串起來——也就是所謂的組合。在 C# 裡可以寫一個接受兩個函數並回傳它們組合的新函數。
// 函數組合器:回傳一個先執行 g、再執行 f 的函數
Func<T, TResult> Compose<T, TIntermediate, TResult>(
Func<TIntermediate, TResult> f,
Func<T, TIntermediate> g)
{
return x => f(g(x));
}
// 範例:
Func<int, int> plusOne = n => n + 1;
Func<int, int> timesTwo = n => n * 2;
var plusOneThenDouble = Compose(timesTwo, plusOne);
Console.WriteLine(plusOneThenDouble(3)); // (3 + 1) * 2 = 8
7. 有用的細節
問題前因:為什麼以前會比較麻煩?
在 delegate 與 lambda 出現之前,開發者常常寫一大堆近似的迴圈,複製程式片段來做「篩選」、「轉換」或「分組」。有了高階函數,就能把可變的行為抽出來放到函數參數裡——這樣能大幅減少重複並提高程式表達力。
一些語法糖:函數當表達式
高階函數經常透過 expression-bodied 方法來實作——簡潔的一行寫法:
List<string> FilterNames(Predicate<string> pred) =>
Names.Where(name => pred(name)).ToList();
List<TResult> MapNames<TResult>(Func<string, TResult> transformer) =>
Names.Select(transformer).ToList();
和 LINQ 機制的比較
來看 LINQ 如何利用高階函數:
| LINQ 方法 | 接受哪個 delegate | 用途 |
|---|---|---|
|
|
過濾元素 |
|
|
轉換元素 |
|
|
依鍵排序 |
|
|
聚合(折疊)集合 |
|
|
按條件檢查元素是否存在 |
所有這些方法都圍繞高階函數這個概念:你寫自己的處理規則,標準函式庫提供「基礎設施」。
8. 可能的錯誤與雷區
對 delegate 型別感到混淆。
一開始可能很難分清哪裡該用 Action、哪裡該用 Func、哪裡該用 Predicate。
提示:如果函數回傳 bool——大概就是 Predicate。如果回傳值——用 Func,如果不回傳任何東西——用 Action。
變數捕獲(closures)。
如果回傳的函數使用了外部環境的變數,要注意在呼叫時變數的值是否是你預期的。變數不是被複製,而是被「捕獲」。
調試複雜管線。
當函數被組合成很長的管線時,很難找出是哪一層把資料處理錯了。加入暫時性輸出與註解會很有幫助:
n => {
Console.WriteLine("操作前:" + n);
var res = n * 2;
Console.WriteLine("操作後:" + res);
return res;
}
GO TO FULL VERSION