CodeGym /課程 /C# SELF /函數式程式設計入門

函數式程式設計入門

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

1. 介紹

函數式程式設計(FP)——是一種程式設計範式,其主要構建單位不是物件也不是程序/方法,而是數學意義上的 函數。在 FP 中主要關注的是描述 「要計算什麼」,而不是 「如何計算」

你已經在使用 lambda 表達式和 LINQ 的時候接觸過 FP 的部分概念。但真正的差別在哪裡?實際上:OOP 描述物件及其交互,程序式編程是一系列步驟,而 FP 是 函數的組合、把行為當作值傳遞、拒絕改變狀態(immutability)並儘量避免副作用。

為什麼要學新的範式?

  • 更乾淨、可預測且易測試的代碼。
  • 簡化的多執行緒支援(「沒狀態就沒問題」)。
  • 簡潔且富表現力(代碼越少,bug 越少)。
  • 高階、易重用的抽象。

類比

想像餐廳接到一個訂單:「做一份歐姆蛋」。命令式的廚師會執行一串指令:拿蛋、打開、攪拌、煎。函數式的廚師會說:res = omlet(yajtsa) — 他用的是函數,抽象掉廚房內部狀態(嗯,差不多啦)。

在 C# 中兩種方式都能用。這讓語言非常靈活且強大——尤其適合真實項目。

FP 的關鍵概念

1. 高階函數

函數可以作為參數傳遞、從其他函數返回、或儲存在變數中。你之前用 lambda 和 delegate 就做過這類事。在 FP 裡,這種「對函數做操作的函數」是基礎。

2. 純函數

如果一個函數的結果僅依賴於參數、並且它不改變外部任何東西(沒有副作用),那它就是「純的」。用相同參數呼叫兩次會得到相同結果。

3. 不可變性 (Immutability)

資料不會「就地變更」:新狀態是新的物件。這大幅簡化對程式的推理,對多執行緒特別有幫助。

4. 無副作用

函數不寫檔、不改變全域變數、不在畫面上畫東西——只是回傳結果。在現實中副作用無法完全避免,但會盡量把它們隔離在系統邊界。

5. 函數組合

可以用其他函數拼出一個函數,就像積木一樣。例如:先過濾出正數,取平方再相加。每個操作都是單獨函數,容易組合(WhereSelectSum)。

2. 在 C# 中的 FP:從理論到實作

C# 是多範式語言:它很好地支援 OOP、程序式以及強大的函數式風格(有 lambda、delegate、擴充方法和 LINQ)。

用我們的教學範例來看

假設我們在開發一個處理數字與字串清單的程式。我們的任務是以函數式風格對這些資料做各種操作。

範例 1:使用高階函數


// 對清單的所有元素應用一個 action
public static void ForEach<T>(List<T> items, Action<T> action)
{
    foreach (var item in items)
    {
        action(item);
    }
}

用法:


var numbers = new List<int> { 1, 2, 3, 4, 5 };
ForEach(numbers, n => Console.WriteLine(n * n)); // 函數當作參數

看到了嗎?函數可以放到變數裡或像普通值一樣傳遞——就像廚房裡的蘋果一樣!

範例 2:純函數

一個不改變程式狀態且只依賴輸入的函數:


int MultiplyByTwo(int x)
{
    return x * 2;
}
  • 不依賴外部任何東西。
  • 不改變外部任何東西。
  • 對於 x = 5 永遠回傳 10。

對比下面這個使用並改變全域變數的函數:


int total = 0;
int AddToTotal(int x)
{
    total += x;
    return total;
}

這就不是純函數——結果依賴外部狀態,且它還改變了那個狀態。

範例 3:資料不可變性

我們不直接改變輸入資料,而是創建新的:


List<int> AddOneToEach(List<int> numbers)
{
    return numbers.Select(n => n + 1).ToList();
}

輸入的清單完全沒有被改變。在多執行緒程式裡這特別方便:鎖和資料競爭問題會少很多。

範例 4:函數組合

取得所有偶數的平方和:


int SumOfEvenSquares(List<int> numbers)
{
    return numbers
        .Where(n => n % 2 == 0)     // 保留偶數
        .Select(n => n * n)         // 平方
        .Sum();                     // 相加
}

可讀且宣告式:每個操作都是單獨的函數。

3. 有用的小細節

FP、LINQ 與 C#

LINQ 幾乎就是「針對集合的 FP 在實作上長什麼樣」:你使用高階函數(WhereSelect 等),得到新的序列而不改變原序列,每次轉換都是單獨的表達式。結果是 IEnumerable<T>,它描述的是 要取得什麼,而不是 如何迭代

類比表

命令式(程序式/OO) 函數式(LINQ/FP 風格)
foreach (var x in xs) ...
xs.Select(...)
「修改」集合 取得新的集合
狀態(total += x 純函數(xs.Sum()
描述為:「做這件事」 描述為:「我們想得到什麼」

FP vs OOP:兩個世界 — 一個 C#

這不是對立的陣營。在真實的 C# 專案中通常會混用:領域模型(domain model)用類別(OOP)建模很方便,而集合處理、資料聚合和轉換則用函數式風格透過 LINQ、lambda 和擴充方法來做。

你對 delegate 的理解會直接有用:Func<T, TResult>Predicate<T>Action<T> 都是 FP 風格的常見基石。

通用的過濾函數:


List<T> Filter<T>(List<T> items, Predicate<T> predicate)
{
    var result = new List<T>();
    foreach (var item in items)
    {
        if (predicate(item))
            result.Add(item);
    }
    return result;
}

呼叫例子:


var adults = Filter(people, person => person.Age >= 18);
var bigFiles = Filter(fileNames, name => name.EndsWith(".mp4") && name.Length > 10);

取代一大堆只有條件不同但功能相似的方法——用一個通用函數即可。

為什麼雇主和面試官喜歡 FP 開發者?

  • FP 有助於在不啟動整個系統的情況下測試小的代碼塊。
  • 邏輯更容易維護:更少的狀態意味著更少的錯誤來源。
  • 更容易寫並行與非同步代碼——沒有全域狀態,競爭條件更少。

怎麼不過度迷信?

是的,FP 很強大。但 C# 並不是純函數式語言,不是所有任務都需要絕對的純度。別害怕在合適場合使用區域變數與合理的變更。重點是可讀性、可預測性與可測試性。FP 元素是工具,而不是教條。

4. 新手常犯的錯誤

很容易碰到看起來像函數式的代碼,但實際上並不符合函數式的原則。

例如,一個函數回傳了新的集合,但在處理過程中偷偷修改了原來的列表——這違反了不可變性的原則,會讓呼叫方的預期被打破。

再比如:lambda 關閉(capture)外部變數並修改它。在函數式觀點裡這就是副作用,會讓代碼行為難以預測。

C# 編譯器不會阻止你這麼做:語言允許這些寫法。因此在 FP 實踐中要注意保證函數「自給自足」,不要改變外部狀態,也不要讀取外部狀態,除了通過它的參數。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION