首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

C# 自訂例外類別完整規範與實作範本

     編輯:ASP.NET 基礎

例外類別繼承體系


System.Exception (所有例外基類)
├── System.SystemException       // CLR 拋出:NullReference, IndexOutOfRange, InvalidOperation...
└── System.ApplicationException  // 應用層自訂例外基類 (建議繼承此類)

⚠️ 微軟現行指引:直接繼承 Exception 即可,ApplicationException 已無特殊語意,但為相容舊碼仍常見。

標準自訂例外必須實作的 4 個建構函式


[Serializable] // 序列化支援 (跨 AppDomain、遠端、日誌持久化必要)
public class BusinessRuleException : Exception {
    // 1. 預設建構函式
    public BusinessRuleException() { }

    // 2. 訊息建構函式
    public BusinessRuleException(string message) : base(message) { }

    // 3. 訊息 + 內部例外 (保留堆疊追蹤鏈)
    public BusinessRuleException(string message, Exception inner) : base(message, inner) { }

    // 4. 序列化建構函式 (反序列化時呼叫)
    protected BusinessRuleException(SerializationInfo info, StreamingContext context) 
        : base(info, context) { }
}

進階擴充:攜帶業務資料的強型別例外


[Serializable]
public class InsufficientInventoryException : Exception {
    public int ProductId { get; }
    public int RequestedQty { get; }
    public int AvailableQty { get; }

    public InsufficientInventoryException(int productId, int requested, int available)
        : base($"商品 {productId} 庫存不足:需求 {requested},可用 {available}") {
        ProductId = productId;
        RequestedQty = requested;
        AvailableQty = available;
    }

    // 保留標準建構函式鏈
    public InsufficientInventoryException() { }
    public InsufficientInventoryException(string message) : base(message) { }
    public InsufficientInventoryException(string message, Exception inner) : base(message, inner) { }
    protected InsufficientInventoryException(SerializationInfo info, StreamingContext context) 
        : base(info, context) {
        ProductId = info.GetInt32(nameof(ProductId));
        RequestedQty = info.GetInt32(nameof(RequestedQty));
        AvailableQty = info.GetInt32(nameof(AvailableQty));
    }

    // 重寫 GetObjectData 確保自訂屬性被序列化
    public override void GetObjectData(SerializationInfo info, StreamingContext context) {
        base.GetObjectData(info, context);
        info.AddValue(nameof(ProductId), ProductId);
        info.AddValue(nameof(RequestedQty), RequestedQty);
        info.AddValue(nameof(AvailableQty), AvailableQty);
    }
}

實戰範例:薪資上限驗證與例外處理流程


public class Employee {
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal CurrentPay { get; private set; }

    public void GiveBonus(decimal amount) {
        var previousPay = CurrentPay;
        CurrentPay += amount;

        if (CurrentPay > 10000m) {
            CurrentPay = previousPay; // 回滾
            throw new PayOverflowException(
                $"員工 {Name} 薪資超過上限 10,000 (目前 {CurrentPay},嘗試增加 {amount})",
                new ArgumentOutOfRangeException(nameof(amount), "薪資調整導致超過上限")
            );
        }
    }
}

[Serializable]
public class PayOverflowException : Exception {
    public PayOverflowException() { }
    public PayOverflowException(string message) : base(message) { }
    public PayOverflowException(string message, Exception inner) : base(message, inner) { }
    protected PayOverflowException(SerializationInfo info, StreamingContext context) : base(info, context) { }
}

例外捕獲與處理最佳實踐

1. 捕獲具體例外,避免吞沒


try {
    emp.GiveBonus(3000);
}
catch (PayOverflowException ex) {
    // 記錄業務違規
    Logger.Warn(ex.Message);
    
    // 顯示友善訊息給使用者
    MessageBox.Show("獎金發放失敗:" + ex.Message);
    
    // 可選:記錄至檔案 (序列化)
    try {
        using (var fs = File.Create("pay_error.log")) {
            var formatter = new BinaryFormatter();
            formatter.Serialize(fs, ex); // 完整保留堆疊、內部例外、自訂屬性
        }
    }
    catch (Exception ioEx) {
        // 日誌寫入失敗不應影響主流程,包裝為新例外拋出或記錄至 EventLog
        throw new PayOverflowException("原始錯誤已處理,但日誌記錄失敗", ioEx);
    }
}
catch (Exception ex) {
    // 未預期錯誤:記錄完整堆疊,拋出或顯示通用錯誤頁
    Logger.Error(ex, "發放獎金時發生未預期錯誤");
    throw; // 保留原始堆疊向上傳播
}

2. 內部例外 (InnerException) 正確運用

  • 捕獲低層例外 (如 SqlException) 時,包裝為業務例外並傳入 inner 參數。
  • 上層處理時可透過 ex.InnerException 追溯根因。
  • 避免 throw new MyEx("msg") 丟失原始堆疊,務必 throw new MyEx("msg", originalEx)。

3. 全域例外處理 (ASP.NET Core / Web Forms / WPF)


// ASP.NET Core Middleware
app.UseExceptionHandler("/Error");
// 或
app.Use(async (context, next) => {
    try { await next(); }
    catch (BusinessRuleException ex) {
        context.Response.StatusCode = 400;
        await context.Response.WriteAsJsonAsync(new { error = ex.Message });
    }
    catch (Exception ex) {
        Logger.Error(ex, "未處理例外");
        context.Response.StatusCode = 500;
        await context.Response.WriteAsJsonAsync(new { error = "系統錯誤,請稍後再試" });
    }
});

命名與設計慣例速查

規範說明範例
類別名稱以 Exception 結尾語意明確InvalidOrderException
標記 [Serializable]支援跨邊界傳遞、日誌持久化✓
實作 4 標準建構函式相容性、序列化、例外鏈完整✓
唯讀屬性攜帶業務資料捕獲端可程式化處理OrderId, ErrorCode
覆寫 GetObjectData / 序列化建構函式自訂屬性正確序列化✓
不在建構函式中拋例外避免建構時期失敗難以診斷✗
避免深層繼承例外階層捕獲複雜度指數上升扁平化為佳

常見反模式


// ❌ 1. 使用 Exception 基類直接拋出 (無法針對性捕獲)
throw new Exception("庫存不足");

// ❌ 2. 吞沒例外不記錄
catch { } 

// ❌ 3. 重新拋出新例外丟失堆疊
catch (Exception ex) { throw new MyException("錯誤"); }

// ❌ 4. 用例外控制正常流程 (效能差、難閱讀)
try { ParseInt(input); } catch { return 0; } // 應用 TryParse

// ✅ 正確:具體例外、保留堆疊、記錄日誌、友善回饋
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved