CodeGym /課程 /C# SELF /在 C# 中實際應用函數式編程

在 C# 中實際應用函數式編程

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

1. 介紹

從理論走向實作,合乎邏輯會問自己:「為什麼我這個 .NET / C# 開發者要學這些函數式編程的技巧?」
的確,C# 並非像 F# 或 Haskell 那樣的純函數式語言。但自 3.0 版起直到 C# 14,它加入了很多 FP 的工具,能顯著提升程式碼的品質與表達力。

以下是它們特別有效的場景:

  • 處理集合 — LINQ、Map/Reduce、過濾、聚合、排序與各種資料「魔法」。
  • 純函數 — 因為降低了狀態與副作用,bug 更少,也更容易除錯。
  • 高階函數 — 通用且可重用的元件,使用起來很舒服。
  • 在多執行緒中使用不可變性 — 是在並行與非同步場景下寫安全程式碼的關鍵之一。
  • 函數組合 — 讓複雜的業務邏輯變得簡潔、易讀且容易測試。

表格:C# 中的 OOP 方法與 FP 方法比較

任務 命令式 (OOP / 舊派) 函數式 (FP)
過濾列表
foreach + if + Add
.Where(predicate)
轉換列表
foreach + 計算 + Add
.Select(lambda)
根據條件查找
foreach + if/return
.FirstOrDefault(predicate)
聚合
用計數變數的迴圈
.Aggregate(seed, func)
快取
手動字典 + 檢查
closure 的函數

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,而不是為了「好看」而使用。

2
任務
C# SELF, 等級 51, 課堂 4
上鎖
使用函數組合處理字串
使用函數組合處理字串
1
問卷/小測驗
函數式編程,等級 51,課堂 4
未開放
函數式編程
函數式編程入門
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION