CodeGym /課程 /C# SELF /高階函數

高階函數

C# SELF
等級 51, 課堂 2
開放

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 用途
Where
Func<T, bool>
過濾元素
Select
Func<T, TResult>
轉換元素
OrderBy
Func<T, TKey>
依鍵排序
Aggregate
Func<TAcc, T, TAcc>
聚合(折疊)集合
Any
Func<T, bool>
按條件檢查元素是否存在

所有這些方法都圍繞高階函數這個概念:你寫自己的處理規則,標準函式庫提供「基礎設施」。

8. 可能的錯誤與雷區

對 delegate 型別感到混淆。
一開始可能很難分清哪裡該用 Action、哪裡該用 Func、哪裡該用 Predicate。
提示:如果函數回傳 bool——大概就是 Predicate。如果回傳值——用 Func,如果不回傳任何東西——用 Action。

變數捕獲(closures)。
如果回傳的函數使用了外部環境的變數,要注意在呼叫時變數的值是否是你預期的。變數不是被複製,而是被「捕獲」。

調試複雜管線。
當函數被組合成很長的管線時,很難找出是哪一層把資料處理錯了。加入暫時性輸出與註解會很有幫助:


n => {
  Console.WriteLine("操作前:" + n);
  var res = n * 2;
  Console.WriteLine("操作後:" + res);
  return res;
}
2
任務
C# SELF, 等級 51, 課堂 2
上鎖
用高階函式做過濾
用高階函式做過濾
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION