☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
C# 基礎 進階

🎯 委派、事件與 Lambda

📌 什麼是委派(Delegate)?

委派就像一張外送訂單。你不需要自己去餐廳拿餐,只要把訂單(方法的參考)交給外送員(委派),他就會幫你執行。

基本委派

// 宣告一個委派型別(定義訂單的格式:接收兩個 int,回傳一個 int)
delegate int MathOperation(int a, int b);

// 定義一個加法方法(符合委派的格式)
static int Add(int x, int y)
{
    return x + y; // 回傳兩數相加的結果
}

// 定義一個乘法方法(也符合委派的格式)
static int Multiply(int x, int y)
{
    return x * y; // 回傳兩數相乘的結果
}

// 使用委派
MathOperation operation = Add; // 把 Add 方法指派給委派(外送員接了加法的單)
int result1 = operation(3, 4); // 呼叫委派,等於呼叫 Add(3, 4),結果為 7

operation = Multiply; // 現在改指派 Multiply 方法(外送員接了乘法的單)
int result2 = operation(3, 4); // 呼叫委派,等於呼叫 Multiply(3, 4),結果為 12

📌 內建泛型委派

C# 提供了三個常用的內建委派,你不需要自己宣告 delegate 型別:

// Action<T>:接收參數但不回傳值(像是下命令)
Action<string> greet = (name) => // 接收一個 string 參數
{
    // 印出歡迎訊息
    Console.WriteLine($"你好,{name}!");
};
greet("小明"); // 呼叫 Action,印出「你好,小明!」

// Func<T, TResult>:接收參數並回傳值(像是問問題然後得到答案)
Func<int, int, int> add = (a, b) => // 接收兩個 int,回傳一個 int
{
    return a + b; // 回傳相加結果
};
int sum = add(10, 20); // 呼叫 Func,得到 30

// Predicate<T>:接收參數並回傳 bool(像是一個判斷條件)
Predicate<int> isEven = (number) => // 接收一個 int
{
    return number % 2 == 0; // 判斷是否為偶數,回傳 true 或 false
};
bool check = isEven(4); // 呼叫 Predicate,得到 true

// 實際應用:用 Predicate 篩選清單
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 }; // 建立數字清單
List<int> evenNumbers = numbers.FindAll(isEven); // 找出所有偶數:2, 4, 6

📌 事件(Events)

事件就像訂閱 YouTube 頻道。當頻道發布新影片(事件觸發),所有訂閱者(事件處理器)都會收到通知。

自訂事件

// 自訂事件參數類別(描述事件的詳細資訊)
public class OrderEventArgs : EventArgs // 繼承 EventArgs
{
    // 訂單編號
    public string OrderId { get; set; }

    // 訂單金額
    public decimal Amount { get; set; }
}

// 訂單處理器類別(YouTube 頻道)
public class OrderProcessor
{
    // 宣告事件(這是頻道的「訂閱」功能)
    public event EventHandler<OrderEventArgs> OrderCompleted;

    // 處理訂單的方法
    public void ProcessOrder(string orderId, decimal amount)
    {
        // 處理訂單邏輯...
        Console.WriteLine($"正在處理訂單 {orderId}..."); // 印出處理中訊息

        // 訂單完成後,觸發事件(發布新影片通知訂閱者)
        OnOrderCompleted(new OrderEventArgs
        {
            OrderId = orderId, // 設定訂單編號
            Amount = amount    // 設定訂單金額
        });
    }

    // 觸發事件的保護方法(標準做法)
    protected virtual void OnOrderCompleted(OrderEventArgs e)
    {
        // 使用 ?. 確保有訂閱者才觸發(避免 null)
        OrderCompleted?.Invoke(this, e); // 通知所有訂閱者
    }
}

// 使用事件
OrderProcessor processor = new OrderProcessor(); // 建立訂單處理器

// 訂閱事件(像是按下 YouTube 的訂閱鈕)
processor.OrderCompleted += (sender, e) =>
{
    // 收到通知後,印出訂單完成的訊息
    Console.WriteLine($"訂單 {e.OrderId} 已完成,金額:{e.Amount}");
};

// 訂閱第二個處理器(寄送通知信)
processor.OrderCompleted += (sender, e) =>
{
    // 收到通知後,寄送 Email
    Console.WriteLine($"已寄送確認信給客戶,訂單:{e.OrderId}");
};

// 處理訂單(觸發事件,通知所有訂閱者)
processor.ProcessOrder("ORD-001", 1500m);

📌 Lambda 表達式

Lambda 就像一張便利貼上的簡短指令,不需要另外寫一個完整的方法。

// 傳統寫法:定義完整的方法
static bool IsPositive(int n)
{
    return n > 0; // 判斷是否為正數
}

// Lambda 寫法:簡潔的匿名方法
Func<int, bool> isPositive = n => n > 0; // n 是參數,n > 0 是回傳值

// 多個參數的 Lambda
Func<int, int, int> multiply = (a, b) => a * b; // 兩個參數相乘

// 多行 Lambda(需要大括號和 return)
Func<int, string> classify = (n) =>
{
    if (n > 0) return "正數";  // 大於 0 回傳正數
    if (n < 0) return "負數";  // 小於 0 回傳負數
    return "零";               // 等於 0 回傳零
};

// Lambda 在 LINQ 中的實際應用
List<int> numbers = new List<int> { -3, -1, 0, 2, 5, 8 }; // 建立數字清單

// 用 Lambda 篩選正數
var positives = numbers.Where(n => n > 0).ToList(); // 結果:2, 5, 8

// 用 Lambda 轉換每個數字
var doubled = numbers.Select(n => n * 2).ToList(); // 每個數字乘以 2

// 用 Lambda 排序
var sorted = numbers.OrderByDescending(n => n).ToList(); // 由大到小排序

📌 閉包(Closure)與捕獲變數

// 閉包:Lambda 可以「記住」外部變數
int multiplier = 3; // 外部變數

Func<int, int> tripler = n => n * multiplier; // Lambda 捕獲了 multiplier 變數

Console.WriteLine(tripler(5)); // 印出 15(5 * 3)

multiplier = 10; // 修改外部變數
Console.WriteLine(tripler(5)); // 印出 50(5 * 10)!因為 Lambda 記住的是變數本身,不是值

🤔 我這樣寫為什麼會錯?

❌ 錯誤 1:事件處理器造成記憶體洩漏

// ❌ 錯誤寫法:訂閱了事件卻忘記取消訂閱
public class Listener // 監聽者類別
{
    public Listener(OrderProcessor processor) // 建構函式
    {
        // 訂閱事件,但從來不取消
        processor.OrderCompleted += OnOrderCompleted; // ❌ 永遠不會被垃圾回收
    }

    private void OnOrderCompleted(object sender, OrderEventArgs e)
    {
        Console.WriteLine("收到訂單通知"); // 處理事件
    }
}
// ✅ 正確寫法:實作 IDisposable,在不需要時取消訂閱
public class Listener : IDisposable // 實作 IDisposable
{
    private readonly OrderProcessor _processor; // 保存處理器的參考

    public Listener(OrderProcessor processor)
    {
        _processor = processor; // 儲存參考以便之後取消訂閱
        _processor.OrderCompleted += OnOrderCompleted; // 訂閱事件
    }

    private void OnOrderCompleted(object sender, OrderEventArgs e)
    {
        Console.WriteLine("收到訂單通知"); // 處理事件
    }

    public void Dispose() // 清理資源
    {
        _processor.OrderCompleted -= OnOrderCompleted; // ✅ 取消訂閱,避免記憶體洩漏
    }
}

解釋: 事件訂閱就像租房子,退租時要把鑰匙還回去。不取消訂閱,物件就永遠不會被回收。

❌ 錯誤 2:迴圈中的閉包陷阱

// ❌ 錯誤寫法:迴圈中捕獲的是變數 i,不是值
List<Action> actions = new List<Action>(); // 建立動作清單
for (int i = 0; i < 3; i++) // 迴圈 0, 1, 2
{
    // 每個 Lambda 都捕獲同一個變數 i
    actions.Add(() => Console.WriteLine(i)); // ❌ 全部都會印出 3!
}
foreach (var action in actions) // 執行所有動作
{
    action(); // 印出 3, 3, 3(不是預期的 0, 1, 2)
}
// ✅ 正確寫法:在迴圈內建立區域變數
List<Action> actions = new List<Action>(); // 建立動作清單
for (int i = 0; i < 3; i++) // 迴圈 0, 1, 2
{
    int captured = i; // ✅ 每次迴圈建立新的區域變數,捕獲當時的值
    actions.Add(() => Console.WriteLine(captured)); // 捕獲 captured 而非 i
}
foreach (var action in actions) // 執行所有動作
{
    action(); // ✅ 正確印出 0, 1, 2
}

解釋: 迴圈變數 i 只有一個,所有 Lambda 共用它。迴圈結束後 i 變成 3,所以全部印 3。建立區域變數就像拍照留念,把當下的值記錄下來。

❌ 錯誤 3:混淆 Func 和 Action

// ❌ 錯誤寫法:Func 需要回傳值,Action 不需要
Func<int, int> doubleIt = (n) =>
{
    Console.WriteLine(n * 2); // ❌ 編譯錯誤!Func 必須有 return
};
// ✅ 正確寫法:根據需求選擇正確的委派型別
// 如果需要回傳值,用 Func
Func<int, int> doubleIt = (n) =>
{
    return n * 2; // ✅ Func 必須回傳值
};

// 如果不需要回傳值,用 Action
Action<int> printDouble = (n) =>
{
    Console.WriteLine(n * 2); // ✅ Action 不需要 return
};

解釋: Func 像是一個有回傳值的「問答題」,Action 像是一個「指令」。搞混就像拿選擇題的答案卡去寫作文。

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊