---
# System prepended metadata

title: C# Delegate 與匿名型別：事件、委派鏈與實戰
tags: [.NET Framework]

---

# C# Delegate 與匿名型別：事件、委派鏈與實戰

## 主題引入與重要性
### 說明
在現代後端系統中，事件驅動架構與非同步流程已成日常。C# 的 delegate（委派）與 event（事件）為建立鬆耦合的通知與回呼機制提供了語言層級的支援；匿名型別則讓我們能以最小成本快速封裝短期資料，尤其在 LINQ 查詢與資料整形時極具價值。對進階後端工程師而言，懂得善用這兩者，不只是語法技巧，更關乎系統可維護性、除錯效率與效能穩定性。然而，委派鏈在多執行緒環境下的執行順序、例外處理與記憶體生命週期，以及匿名型別的型別邊界與配對規則，都可能在高負載或複雜場景引發微妙且難以復現的問題。本文將以實務角度，結合理論與範例，提供一套可應用、可重構的操作指南。

### 重點列表
- 事件驅動與非同步：委派與事件讓回呼流程更安全並具可測試性，適合高彈性、鬆耦合的後端模組間協作。
- 委派鏈的風險：多個訂閱者的組合執行順序、單一處理常式拋出例外的影響、跨執行緒觸發的同步問題，是除錯關鍵。
- 匿名型別的價值與限制：極速封裝臨時結果（尤其 LINQ），但不宜跨方法邊界，避免模糊 API 合約與難以維護。
- 效能與可觀測性：避免在熱路徑頻繁建立匿名型別，委派觸發需考量同步內容（SynchronizationContext）與執行緒安全。
- 除錯導向設計：以可記錄、可追蹤的事件封裝、命名與解耦策略，讓生產問題快速定位。

### 補充範例／常見問答
```csharp
using System;
using System.Collections.Generic;
using System.Linq;

public class Intro
{
    // 定義一個委派，描述「兩數相加」這種行為的簽章
    public delegate int SumDelegate(int a, int b);

    public static void Main()
    {
        // 1) 使用委派包裝一個純函式
        SumDelegate sum = (x, y) => x + y; // 使用 Lambda 建立委派實例
        int r = sum(3, 5);                 // 呼叫委派，就像呼叫方法
        Console.WriteLine($"3 + 5 = {r}"); // 輸出 8

        // 2) 使用匿名型別在 LINQ 中快速封裝結果
        var items = new List<int> { 1, 2, 3, 4 };
        var projection = items.Select(n => new 
        {
            Original = n,           // 原值
            IsEven = n % 2 == 0,    // 是否偶數
            Square = n * n          // 平方
        });

        // 匿名型別通常只在目前範圍內消費（例如立即列印或傳給下一個 LINQ）
        foreach (var row in projection)
        {
            Console.WriteLine($"n={row.Original}, Even={row.IsEven}, Square={row.Square}");
        }
    }
}
```

- Q：匿名型別可以作為方法回傳型別嗎？  
  A：不建議。匿名型別是編譯器產生的內部型別，無法在強型別 API 合約中穩定表達。若需要回傳，請定義 record/類別或使用 ValueTuple（具名欄位）來維持可讀性與相容性。

---

## Delegate 基礎與進階概念
### 說明
Delegate 是型別安全的函式指標，讓方法能以資料形式被傳遞與呼叫。搭配 event，可建立可控的發布/訂閱模型。對進階工程師而言，關鍵在於：理解委派鏈（多個訂閱者）、例外處理策略、同步與非同步呼叫差異、以及在多執行緒環境中維持可預期性與可觀測性。

### 重點列表
- 定義與用法：delegate 宣告一種方法簽章；可用具名方法或 Lambda 指派；呼叫時與一般方法無異。
- 委派鏈（Multicast）：同一委派可包含多個目標；呼叫時依註冊順序逐一執行；任何一個處理常式拋出例外都可能中斷後續。
- 同步 vs 非同步：同步呼叫簡單但可能阻塞；非同步呼叫（Task、async/await）需注意例外收斂與同步內容。
- 事件封裝：event 限制外部只能 += 或 -=，避免被覆寫整個委派參考，提升封裝與安全性。
- 除錯觀點：保存快照（複製委派到區域變數）以避免競態；對 InvocationList 迴圈個別 try-catch；加上來源識別資訊提升追蹤性。

### 補充範例／常見問答
```csharp
using System;
using System.Threading.Tasks;

public class Delegates101
{
    // 定義一種委派簽章：接受 int，回傳 int
    public delegate int Transformer(int x);

    // 同步示範
    public static int Double(int x) => x * 2; // 具名方法

    public static async Task Main()
    {
        // 1) 建立委派實例（具名方法 + Lambda 混合）
        Transformer t = Double;          // 指到具名方法
        t += (x) => x + 10;              // 加入另一個處理常式，形成「委派鏈」

        // 呼叫委派鏈：依序執行 Double -> Lambda
        // 注意：只有最後一個回傳值會被保留（此處為 x+10 的結果）
        int result = t(5);
        Console.WriteLine($"Result of chain: {result}"); // 輸出 15

        // 2) 非同步委派：以 Func<Task> 或 Func<T, Task<R>> 表達
        Func<int, Task<int>> asyncWork = async (n) =>
        {
            await Task.Delay(50);    // 模擬 I/O
            return n * n;            // 回傳平方
        };

        int squared = await asyncWork(7);   // 非同步等待結果
        Console.WriteLine($"7^2 = {squared}");

        // 3) 安全地呼叫委派鏈：個別捕捉例外，避免單一失敗殃及其他
        Transformer chain = null;
        chain += (x) => x + 1;
        chain += (x) => throw new InvalidOperationException("測試例外");
        chain += (x) => x * 3;

        // 取快照避免在呼叫過程中被異動（例如其他執行緒 -= 訂閱）
        var snapshot = chain;
        if (snapshot != null)
        {
            foreach (Transformer handler in snapshot.GetInvocationList())
            {
                try
                {
                    int val = handler(2);
                    Console.WriteLine($"Handler OK => {val}");
                }
                catch (Exception ex)
                {
                    Console.WriteLine($"Handler failed: {ex.Message}");
                }
            }
        }
    }
}
```

- Q：為什麼委派鏈只回傳最後一個處理常式的回傳值？  
  A：MulticastDelegate 的語義是依序呼叫所有目標，整體呼叫結果是最後一次委派呼叫的回傳值。若需要彙總多個結果，請自行迴圈 GetInvocationList() 並蒐集回傳值，或改用事件模型傳遞上下文與結果容器。

---

## 事件機制與委派鏈的實戰應用
### 說明
事件進一步限制了委派的可見性：外部只能訂閱/取消訂閱，而不能直接指派委派，避免外部覆蓋既有的訂閱。實務上，事件廣用於通知狀態改變（如任務完成、緩存更新）。在多執行緒或非同步情境下，必須處理競態條件、事件風暴、例外隔離與訂閱者漏釋放導致的記憶體洩漏。

### 重點列表
- 設計事件通知系統：以 EventArgs 或自訂不可變的 Payload，保持 API 穩定；命名清晰如 SomethingHappened。
- 安全觸發：複製快照、使用 ?.Invoke、或手動迭代委派鏈並個別 try-catch，避免單點失敗。
- 執行緒策略：釐清觸發執行緒與處理執行緒；必要時用 Task.Run 或 Channel/Queue 將處理轉移背景執行緒。
- 訂閱者管理：訂閱與移除需對稱；長壽命物件訂閱短壽命來源易洩漏，考慮弱事件或集中式事件匯流排。
- 觀測與壓制：在高頻事件中加入節流/緩衝，並對事件來源打點（計數、耗時、錯誤率）便於營運監控。

### 補充範例／常見問答
```csharp
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;

public class MessageEventArgs : EventArgs
{
    public string Topic { get; }
    public string Payload { get; }

    public MessageEventArgs(string topic, string payload)
    {
        Topic = topic;     // 主題
        Payload = payload; // 訊息內容
    }
}

public class EventBus
{
    // 自訂事件：外部只能 += 或 -=，無法直接賦值覆蓋
    public event EventHandler<MessageEventArgs> MessagePublished;

    // 觸發事件的安全方法
    protected virtual void OnMessagePublished(MessageEventArgs e)
    {
        // 取快照，避免觸發期間委派鏈被修改造成 race condition
        var snapshot = MessagePublished;
        if (snapshot == null) return;

        foreach (EventHandler<MessageEventArgs> handler in snapshot.GetInvocationList())
        {
            try
            {
                handler(this, e); // 個別保護，避免單一失敗中斷整體通知
            }
            catch (Exception ex)
            {
                // 在生產中應寫入記錄系統（例如結合事件名稱與訂閱者識別）
                Console.Error.WriteLine($"Handler error: {ex.Message}");
            }
        }
    }

    // 非同步發布：將事件轉移到背景執行緒處理，避免阻塞呼叫端
    public Task PublishAsync(string topic, string payload, CancellationToken ct = default)
    {
        var e = new MessageEventArgs(topic, payload);
        return Task.Run(() => OnMessagePublished(e), ct);
    }
}

public class Demo
{
    public static async Task Main()
    {
        var bus = new EventBus();

        // 訂閱者 A：快速處理
        bus.MessagePublished += (s, e) =>
        {
            Console.WriteLine($"A received: {e.Topic} - {e.Payload}");
        };

        // 訂閱者 B：故意丟例外，觀察不影響其他訂閱者
        bus.MessagePublished += (s, e) =>
        {
            if (e.Topic == "Error") throw new InvalidOperationException("B failed");
            Console.WriteLine("B OK");
        };

        // 訂閱者 C：模擬較慢處理
        bus.MessagePublished += (s, e) =>
        {
            Thread.Sleep(20); // 模擬 I/O 或計算
            Console.WriteLine("C done");
        };

        await bus.PublishAsync("Info", "Hello");
        await bus.PublishAsync("Error", "Oops"); // B 會失敗，但 A、C 照常執行
    }
}
```

- Q：為什麼事件會造成記憶體洩漏？  
  A：事件是強參考；若長壽命物件（Publisher）持有短壽命物件（Subscriber）的處理常式參考，短壽命物件即使不再使用也無法被垃圾回收。解法：在適當時機 -= 取消訂閱、使用弱事件（WeakEvent pattern）、或集中式事件匯流排管理訂閱生命週期。

---

## 匿名型別的宣告與應用實務
### 說明
匿名型別是編譯器產生的不可變（預設唯讀）型別，常用於 LINQ 的 Select 投影或臨時資料整形。它讓我們省去定義 DTO 的負擔，但必須控制使用範圍，避免滲入公開 API，造成維護與相容性風險。

### 重點列表
- 宣告與語法：使用 new { Prop = expr, ... } 建立；編譯器推斷型別，並產生 Equals/GetHashCode 基於屬性值。
- 常見場景：LINQ 投影、群組匯總、日誌結構化欄位；在單一方法或查詢管線內快速消費。
- 限制與取捨：不可命名公開型別、不可跨方法/模組作為合約；在熱路徑大量建立會造成 GC 壓力。
- 可替代方案：ValueTuple（(X: int, Y: int)）或 record 做為穩定的 API 合約；匿名型別可用於過渡，後續重構為明確型別。
- 陷阱：兩個匿名型別相等的條件是「屬性名稱與順序與型別」完全相同；否則即使欄位同名同值也不是相同型別。

### 補充範例／常見問答
```csharp
using System;
using System.Collections.Generic;
using System.Linq;

public class AnonymousTypesDemo
{
    public static void Main()
    {
        var products = new[]
        {
            new { Name = "Pen", Price = 10m, Qty = 3 },   // 這裡也使用匿名型別做示範
            new { Name = "Book", Price = 80m, Qty = 1 },
            new { Name = "Bag", Price = 120m, Qty = 2 },
        };

        // 1) LINQ 投影：建立另一個匿名型別，包含總價與標籤
        var report = products.Select(p => new
        {
            p.Name,                         // 習慣：沿用來源名稱，易讀
            Total = p.Price * p.Qty,        // 計算欄位
            Tag = p.Price > 50 ? "High" : "Low"
        });

        foreach (var row in report)
        {
            Console.WriteLine($"{row.Name}: {row.Total} ({row.Tag})");
        }

        // 2) 不要把匿名型別外露成公開 API：以下僅在方法內部使用
        var top = report.Where(x => x.Total >= 100)
                        .OrderByDescending(x => x.Total)
                        .FirstOrDefault();

        Console.WriteLine($"Top: {top?.Name} -> {top?.Total}");
    }
}
```

- Q：匿名型別可以當作 Dictionary 的鍵嗎？  
  A：可以，因為匿名型別實作了基於所有屬性值的 Equals/GetHashCode。但要小心生命週期與型別邊界，且在 API 合約中不建議使用，避免未來重構造成相容性問題。若需公開合約，請使用 record 或具名的不可變型別。

---

## 最佳實踐、除錯技巧與效能調優
### 說明
當系統成長，委派與匿名型別的使用將遍布各層。良好實踐能降低追查時間與效能波動，協助團隊維持一緻性與可觀測性。本節聚焦如何平衡彈性與複雜性，並提供可落地的除錯與調優策略。

### 重點列表
- 委派與事件的設計守則：使用明確命名、固定簽章（EventHandler<T>）、不可變 EventArgs；事件觸發時紀錄來源、延遲、例外。
- 安全觸發模板：快照 + 個別 try-catch + 超時與計數，必要時以背景處理隔離對呼叫端的影響。
- 訂閱生命週期：集中管理訂閱與釋放；在服務啟停或作用域（Scope）結束時統一清理。
- 匿名型別效能：避免在熱路徑大量分配；對高頻路徑改用 struct record 或 ValueTuple；避免在迴圈中隱含閉包捕捉外部變數。
- 除錯工具與可視化：利用 IDE 的「平行堆疊」、「委派目標檢視」、日誌關聯 ID（CorrelationId），快速定位委派鏈與事件風暴。

### 補充範例／常見問答
```csharp
using System;
using System.Diagnostics;
using System.Threading.Tasks;

public class SafeEvent<T> where T : EventArgs
{
    public event EventHandler<T> Occurred;

    // 安全觸發模板：度量 + 隔離例外
    public void Raise(object sender, T args, Action<Exception> onError = null)
    {
        var snapshot = Occurred;
        if (snapshot == null) return;

        foreach (EventHandler<T> handler in snapshot.GetInvocationList())
        {
            var sw = Stopwatch.StartNew();
            try
            {
                handler(sender, args);                 // 執行處理常式
            }
            catch (Exception ex)
            {
                onError?.Invoke(ex);                   // 回報錯誤（寫日志或計量）
            }
            finally
            {
                sw.Stop();
                if (sw.ElapsedMilliseconds > 100)      // 簡單門檻偵測慢 handler
                {
                    Console.WriteLine($"Slow handler: {handler.Method.Name} took {sw.ElapsedMilliseconds} ms");
                }
            }
        }
    }
}

public class DemoBestPractice
{
    public static async Task Main()
    {
        var ev = new SafeEvent<EventArgs>();
        ev.Occurred += (s, e) => Console.WriteLine("Fast");
        ev.Occurred += (s, e) => { Task.Delay(150).Wait(); Console.WriteLine("Slow"); };

        ev.Raise(null, EventArgs.Empty, ex => Console.Error.WriteLine(ex.Message));
    }
}
```

- Q：如何追蹤「多重訂閱」造成的重複執行？  
  A：在訂閱時加入唯一識別（例如透過封裝方法，記錄 handler.Method/Target 與事件名稱），或建立中介層（Event Subscription Registry）去重與檢查重複註冊。發現重複後紀錄告警並拒絕再次註冊。

---

## 綜合案例實作與重構策略
### 說明
本案例示範一個簡化的「訂單處理系統」：當新訂單進入，系統透過事件通知多個模組（計價、庫存、審計記錄）。我們將展示如何管理委派鏈、控制例外對隔離域的影響、以匿名型別進行查詢整形，以及最終將匿名型別重構為穩定的合約型別。

### 重點列表
- 事件驅動流程：OrderCreated 事件通知三個處理模組，彼此無耦合。
- 委派鏈治理：個別 try-catch，失敗不波及其他；慢訂閱者監測與分流。
- 匿名型別過渡：查詢合併時使用匿名型別，對外 API 改以 record 發佈。
- 重構策略：當匿名型別被重複使用或跨界傳遞時，立即提煉為具名型別；在熱路徑用 ValueTuple/struct 減少配置。
- 效能瓶頸定位：對事件處理計時，出現長尾時做分流（背景隊列）、或加快 I/O（快取/批次）。

### 補充範例／常見問答
```csharp
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;

// 1) 事件與資料契約
public record Order(string Id, string Sku, int Qty, decimal UnitPrice);

public class OrderCreatedEventArgs : EventArgs
{
    public Order Order { get; }
    public OrderCreatedEventArgs(Order order) => Order = order;
}

public class OrderBus
{
    public event EventHandler<OrderCreatedEventArgs> OrderCreated;

    public void Publish(Order order, Action<Exception> onError = null)
    {
        var snapshot = OrderCreated;
        if (snapshot == null) return;

        foreach (EventHandler<OrderCreatedEventArgs> handler in snapshot.GetInvocationList())
        {
            var sw = Stopwatch.StartNew();
            try
            {
                handler(this, new OrderCreatedEventArgs(order));
            }
            catch (Exception ex)
            {
                onError?.Invoke(ex); // 隔離錯誤
            }
            finally
            {
                sw.Stop();
                if (sw.ElapsedMilliseconds > 50)
                    Console.WriteLine($"Slow handler: {handler.Method.Name} took {sw.ElapsedMilliseconds} ms");
            }
        }
    }
}

// 2) 訂閱者：計價、庫存、審計
public class PricingService
{
    public void OnOrderCreated(object? sender, OrderCreatedEventArgs e)
    {
        // 使用匿名型別進行查詢整形（僅在此方法內消費）
        var line = new
        {
            e.Order.Id,
            Subtotal = e.Order.UnitPrice * e.Order.Qty,
            Discount = e.Order.Qty >= 10 ? 0.1m : 0m,
        };
        var total = line.Subtotal * (1 - line.Discount);
        Console.WriteLine($"[Pricing] Order={line.Id}, Total={total}");
    }
}

public class InventoryService
{
    public void OnOrderCreated(object? sender, OrderCreatedEventArgs e)
    {
        // 假裝檢查庫存
        Console.WriteLine($"[Inventory] Reserve {e.Order.Qty} for {e.Order.Sku}");
        // 若失敗可丟出例外，將被上層隔離
    }
}

public class AuditService
{
    public void OnOrderCreated(object? sender, OrderCreatedEventArgs e)
    {
        // 寫入審計記錄（同步簡化示範）
        Console.WriteLine($"[Audit] Order received: {e.Order.Id}");
    }
}

// 3) 整合與重構
public class Program
{
    public static async Task Main()
    {
        var bus = new OrderBus();
        var pricing = new PricingService();
        var inv = new InventoryService();
        var audit = new AuditService();

        // 訂閱
        bus.OrderCreated += pricing.OnOrderCreated;
        bus.OrderCreated += inv.OnOrderCreated;
        bus.OrderCreated += audit.OnOrderCreated;

        // 發布
        bus.Publish(new Order("O-1001", "SKU-ABC", 12, 50m), ex => Console.Error.WriteLine(ex));

        // 重構建議：
        // - 若 PricingService 中的匿名型別被多次使用，改為定義 record PricingResult(string OrderId, decimal Subtotal, decimal Discount, decimal Total)
        // - 若 Inventory 與 Audit 處理變慢，改以背景隊列（Channel/Queue）非同步處理並批次寫入
        await Task.CompletedTask;
    }
}
```

- Q：何時該把匿名型別重構為具名型別？  
  A：當相同匿名結構在多處出現、跨方法傳遞、或需要公開為 API 合約時，立即提煉為 record/類別。這能提升可讀性、可重用性與型別安全，並利於序列化與版本治理。

---

## 延伸閱讀與資源總結
### 說明
本節彙整委派、事件與匿名型別的設計要點，以及延伸學習的高品質資源，協助你在面對更複雜的非同步事件流與資料整形需求時，有可依循的參考。

### 重點列表
- 設計哲學：用委派與事件解耦，用明確契約界定資料邊界；匿名型別適合「近場使用」，跨界即刻提煉。
- 工具與方法：以快照、安全觸發、個別 try-catch、計量與紀錄形成標準模板；以基準測試（BenchmarkDotNet）與分析器（Analyzer）守護品質。
- 學習路徑：先掌握語法與行為，再進一步探討多執行緒一致性與可觀測性落地策略。

### 補充範例／常見問答
- 推薦資源：
  - Microsoft Docs: Delegates, Events, Anonymous Types
  - .NET Blog 與 Channel 9 深入解說事件與非同步模式
  - 開源專案：DotNetRuntime（觀察事件與 GC 行為）、EventStore 範例
  - 書籍：Concurrency in C# Cookbook、Pro .NET Memory Management
- Q：如何在不破壞既有訂閱者的前提下演進事件的負載內容？  
  A：使用版本化的 EventArgs（例如 OrderCreatedV2EventArgs 繼承原版本或新增屬性），並暫時同時發布兩個事件；或採用單一事件名稱但 EventArgs 向下相容，確保舊訂閱者仍可運作。逐步淘汰舊版事件。

---

## 結論
委派與事件讓我們能以語言原生的方式實作回呼與發布/訂閱，對於鬆耦合架構、非同步流程與跨模組協作至關重要；匿名型別則在資料整形與 LINQ 場景提供高效率的臨時封裝。實務上，請以安全觸發模板（快照、個別 try-catch、計量）、嚴謹的訂閱生命週期管理，以及清晰可觀測的日誌策略，降低委派鏈在多執行緒下的風險。同時，將匿名型別限制在近場使用，一旦跨界或重複使用即刻提煉為具名型別（record/DTO），在效能敏感路徑則改用 ValueTuple/struct。建議你從小型事件流開始套用本文模板，逐步導入到核心服務，並以基準與監控數據持續調整。當你能在「彈性、可維護性、效能」三者間取得良好平衡，委派與匿名型別將成為你後端系統設計的可靠基石。