C# 異常繼承從設(shè)計(jì)原則到sealed 關(guān)鍵字全解析
引言:異常,不僅僅是 try-catch
在 C# 開(kāi)發(fā)中,異常處理是最基礎(chǔ)卻最容易被忽視的高級(jí)話題。很多開(kāi)發(fā)者掌握了 try-catch-finally 的語(yǔ)法,卻對(duì)異常的設(shè)計(jì)哲學(xué)知之甚少。本文將深入探討自定義異常繼承的設(shè)計(jì)原則、實(shí)戰(zhàn)應(yīng)用,以及一個(gè)看似矛盾的規(guī)范——為什么自定義異常通常要標(biāo)記為 sealed?
一、Exception 繼承體系全景圖
1.1 核心繼承層次
System.Object
└── System.Exception (抽象基類)
├── System.SystemException (CLR 拋出的異常)
│ ├── NullReferenceException
│ ├── IndexOutOfRangeException
│ └── ...
├── System.ApplicationException (應(yīng)用程序異常 - 已過(guò)時(shí))
└── 自定義異常 (繼承自 Exception)1.2 關(guān)鍵成員解析
public class Exception : ISerializable
{
// 核心屬性
public string Message { get; } // 異常說(shuō)明
public Exception InnerException { get; } // 內(nèi)部異常(鏈?zhǔn)剑?
public string StackTrace { get; } // 調(diào)用堆棧
public MethodBase TargetSite { get; } // 拋出異常的方法
public IDictionary Data { get; } // 額外鍵值對(duì)數(shù)據(jù)
// 核心方法
public virtual void GetObjectData(SerializationContext context);
public Exception GetBaseException(); // 獲取最內(nèi)部異常
}二、實(shí)戰(zhàn):設(shè)計(jì)一個(gè)三層架構(gòu)的自定義異常體系
2.1 業(yè)務(wù)場(chǎng)景:電商訂單系統(tǒng)
假設(shè)我們有一個(gè)訂單處理系統(tǒng),需要在不同層級(jí)拋出有意義的異常:
// 1. 基礎(chǔ)自定義異常(抽象基類)
public abstract class OrderProcessingException : Exception
{
public string OrderId { get; }
protected OrderProcessingException(string message, string orderId)
: base(message)
{
OrderId = orderId;
}
protected OrderProcessingException(string message, string orderId, Exception inner)
: base(message, inner)
{
OrderId = orderId;
}
// 支持序列化(用于跨 AppDomain 傳遞)
protected OrderProcessingException(
SerializationInfo info, StreamingContext context)
: base(info, context)
{
OrderId = info.GetString("OrderId");
}
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue("OrderId", OrderId);
}
}
// 2. 具體業(yè)務(wù)異常 - 訂單驗(yàn)證失敗
public sealed class OrderValidationException : OrderProcessingException
{
public List<string> ValidationErrors { get; }
public OrderValidationException(string orderId, List<string> errors)
: base($"訂單 {orderId} 驗(yàn)證失敗: {string.Join(", ", errors)}", orderId)
{
ValidationErrors = errors;
}
}
// 3. 庫(kù)存不足異常
public sealed class InsufficientInventoryException : OrderProcessingException
{
public string ProductId { get; }
public int RequestedQuantity { get; }
public int AvailableQuantity { get; }
public InsufficientInventoryException(string orderId, string productId,
int requested, int available)
: base($"產(chǎn)品 {productId} 庫(kù)存不足 (需要: {requested}, 可用: {available})",
orderId)
{
ProductId = productId;
RequestedQuantity = requested;
AvailableQuantity = available;
}
}
// 4. 支付失敗異常
public sealed class PaymentFailedException : OrderProcessingException
{
public string PaymentTransactionId { get; }
public decimal Amount { get; }
public string FailureReason { get; }
public PaymentFailedException(string orderId, string transactionId,
decimal amount, string reason)
: base($"訂單 {orderId} 支付失敗: {reason}", orderId)
{
PaymentTransactionId = transactionId;
Amount = amount;
FailureReason = reason;
}
}2.2 使用示例:分層異常處理
public class OrderService
{
private readonly IOrderRepository _repository;
private readonly IInventoryService _inventory;
private readonly IPaymentGateway _payment;
public async Task ProcessOrderAsync(string orderId)
{
try
{
// 1. 驗(yàn)證訂單
var order = await _repository.GetOrderAsync(orderId);
ValidateOrder(order);
// 2. 檢查庫(kù)存
await ReserveInventoryAsync(order);
// 3. 處理支付
await ProcessPaymentAsync(order);
// 4. 更新訂單狀態(tài)
await _repository.UpdateOrderStatusAsync(orderId, "Completed");
}
catch (OrderValidationException ex)
{
// 業(yè)務(wù)層處理:記錄驗(yàn)證失敗,通知用戶修改訂單
_logger.LogWarning(ex, "訂單驗(yàn)證失敗");
throw; // 重新拋出,讓上層 UI 處理
}
catch (InsufficientInventoryException ex)
{
// 嘗試自動(dòng)替換供應(yīng)商或部分發(fā)貨
await HandleInventoryShortage(ex);
throw; // 仍需要通知調(diào)用方
}
catch (PaymentFailedException ex)
{
// 記錄支付失敗,嘗試其他支付方式
await TryAlternativePayment(ex);
throw;
}
catch (Exception ex)
{
// 捕獲未知異常,包裝為業(yè)務(wù)異常
throw new OrderProcessingException(
$"處理訂單 {orderId} 時(shí)發(fā)生未知錯(cuò)誤", orderId, ex);
}
}
private void ValidateOrder(Order order)
{
var errors = new List<string>();
if (string.IsNullOrEmpty(order.CustomerId))
errors.Add("客戶ID不能為空");
if (order.TotalAmount <= 0)
errors.Add("訂單金額必須大于0");
if (errors.Any())
throw new OrderValidationException(order.Id, errors);
}
}2.3 UI 層優(yōu)雅處理
[ApiController]
public class OrderController : ControllerBase
{
[HttpPost("{orderId}/process")]
public IActionResult ProcessOrder(string orderId)
{
try
{
await _orderService.ProcessOrderAsync(orderId);
return Ok(new { message = "訂單處理成功" });
}
catch (OrderValidationException ex)
{
// 返回 400 并附帶驗(yàn)證詳情
return BadRequest(new
{
error = ex.Message,
validationErrors = ex.ValidationErrors,
orderId = ex.OrderId
});
}
catch (InsufficientInventoryException ex)
{
// 返回 409 Conflict,提示用戶調(diào)整數(shù)量
return Conflict(new
{
error = ex.Message,
productId = ex.ProductId,
available = ex.AvailableQuantity
});
}
catch (PaymentFailedException ex)
{
// 返回 402 Payment Required
return StatusCode(402, new { error = ex.Message });
}
catch (OrderProcessingException ex)
{
// 通用業(yè)務(wù)異常
return StatusCode(500, new { error = ex.Message });
}
}
}三、核心爭(zhēng)議:為什么自定義異常要 sealed?
3.1 微軟官方設(shè)計(jì)準(zhǔn)則的明確規(guī)定
CA1064: Exceptions should be public
CA1032: Implement standard exception constructors
Do seal exception classes - 雖然沒(méi)有獨(dú)立的 CA 代碼,但 .NET Core 源碼分析和 Framework Design Guidelines 明確建議異常類應(yīng)為 sealed。
3.2 Sealed 的四大核心理由
理由 1:防止異常多態(tài)的濫用
// 反模式 - 不應(yīng)該這樣做
public class DatabaseException : Exception { }
// 有人繼承了它,改變了語(yǔ)義
public class SqlConnectionException : DatabaseException { }
public class SqlQueryException : DatabaseException { }
// 問(wèn)題:catch(DatabaseException ex) 會(huì)捕獲所有子類
// 導(dǎo)致無(wú)法精確處理特定錯(cuò)誤如果確實(shí)需要層次結(jié)構(gòu),應(yīng)該使用不同的異常類型,而不是繼承:
// 正確做法:獨(dú)立的不相關(guān)異常
public sealed class SqlConnectionException : Exception { }
public sealed class SqlQueryException : Exception { }
理由 2:保持異常語(yǔ)義的原子性
// 未密封的異常
public class FileOperationException : Exception
{
public string FilePath { get; set; }
}
// 派生類可能修改重要屬性
public class SpecialFileException : FileOperationException
{
// 可能覆蓋 FilePath 的語(yǔ)義,導(dǎo)致基類邏輯錯(cuò)誤
public new string FilePath { get; set; }
}
// 問(wèn)題:基類的異常處理代碼可能被破壞理由 3:序列化與跨域邊界傳遞
// 未密封的異常在跨 AppDomain 或跨進(jìn)程序列化時(shí)
// 需要完整的類型信息,派生類可能破壞序列化契約
[Serializable]
public class MyException : Exception
{
// 如果沒(méi)有正確實(shí)現(xiàn)序列化構(gòu)造函數(shù),派生類會(huì)失敗
protected MyException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
}
// 派生類可能忘記實(shí)現(xiàn)序列化構(gòu)造函數(shù)
public class DerivedException : MyException { } // 危險(xiǎn)!理由 4:性能與代碼穩(wěn)定性
// JIT 編譯器能對(duì) sealed 類進(jìn)行更好的優(yōu)化
// 調(diào)用虛方法時(shí)無(wú)需檢查派生類
public sealed class FastException : Exception
{
public override string Message => "Optimized";
}
// vs 未密封版本
public class VirtualException : Exception
{
public override string Message => "Needs vtable lookup";
}3.3 什么時(shí)候可以不用 sealed?
極少數(shù)例外場(chǎng)景:
// 1. 抽象基類模式(本身不直接拋出)
public abstract class PluginException : Exception
{
protected PluginException(string message) : base(message) { }
}
// 2. 框架級(jí)別的公共異常基類(如 Prism 的 CompositePresentationException)
// 但這通常被認(rèn)為是反模式
// 3. 測(cè)試 Mock 時(shí)需要(但測(cè)試應(yīng)避免 Mock 異常)3.4 實(shí)戰(zhàn)對(duì)比:密封 vs 非密封
// ? 推薦:密封的完整異常
public sealed class ApiException : Exception
{
public int StatusCode { get; }
public string ApiPath { get; }
public ApiException(string message, int statusCode, string apiPath)
: base(message) => (StatusCode, ApiPath) = (statusCode, apiPath);
private ApiException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
StatusCode = info.GetInt32(nameof(StatusCode));
ApiPath = info.GetString(nameof(ApiPath));
}
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);
info.AddValue(nameof(StatusCode), StatusCode);
info.AddValue(nameof(ApiPath), ApiPath);
}
}
// 使用:清晰、安全、高性能
try { /* ... */ }
catch (ApiException ex) when (ex.StatusCode == 404)
{
// 精確匹配,無(wú)需擔(dān)心子類干擾
}四、最佳實(shí)踐總結(jié)
4.1 設(shè)計(jì)檢查清單
- 異常類命名為
[Name]Exception - 標(biāo)記為
sealed(除非有充分理由) - 實(shí)現(xiàn)三個(gè)標(biāo)準(zhǔn)構(gòu)造函數(shù)(參數(shù)為 message、message+inner、序列化)
- 添加自定義屬性時(shí)實(shí)現(xiàn)序列化支持
- 避免在異常屬性中使用復(fù)雜的引用類型
- 異常類應(yīng)該是
public(跨程序集使用)
4.2 構(gòu)造函數(shù)模板
public sealed class MyException : Exception
{
// 1. 無(wú)參構(gòu)造函數(shù)(可選)
public MyException() { }
// 2. 帶消息的構(gòu)造函數(shù)
public MyException(string message) : base(message) { }
// 3. 帶內(nèi)部異常的構(gòu)造函數(shù)
public MyException(string message, Exception inner) : base(message, inner) { }
// 4. 序列化構(gòu)造函數(shù)(必須)
private MyException(SerializationInfo info, StreamingContext context)
: base(info, context) { }
}4.3 拋異常 vs 返回值
// ? 避免:用返回碼表示錯(cuò)誤
public enum Result { Success, NotFound, ValidationError }
public Result ProcessOrder(string id) { /* ... */ }
// ? 推薦:使用異常
public void ProcessOrder(string id)
{
if (string.IsNullOrEmpty(id))
throw new ArgumentNullException(nameof(id));
// ...
}
// ? 邊界情況:預(yù)期內(nèi)的失敗用 Result 模式
public (bool Success, string ErrorMessage) TryParseOrder(string input) { /* ... */ }五、性能考量與替代方案
5.1 異常的性能開(kāi)銷
// 異常很昂貴:堆棧跟蹤收集 + 序列化 + CLR 內(nèi)部處理
// 100,000 次異常拋出 ≈ 2-3 秒
// 100,000 次條件判斷 ≈ 0.01 秒
// ? 高頻路徑避免異常
public bool TryGetValue(string key, out string value)
{
if (_cache.ContainsKey(key))
{
value = _cache[key];
return true;
}
value = null;
return false;
}
// 而不是
public string GetValue(string key)
{
if (!_cache.ContainsKey(key))
throw new KeyNotFoundException(); // 如果頻繁發(fā)生,性能災(zāi)難
return _cache[key];
}5.2 何時(shí)真正需要自定義異常
- ? 需要攜帶額外的業(yè)務(wù)數(shù)據(jù)(如訂單ID、產(chǎn)品ID)
- ? 需要在日志系統(tǒng)中區(qū)分不同業(yè)務(wù)場(chǎng)景
- ? 需要特定于領(lǐng)域的中文錯(cuò)誤信息
- ? 需要與第三方系統(tǒng)集成時(shí)的錯(cuò)誤映射
- ? 僅僅為了給異常起個(gè)新名字
- ? 可以使用現(xiàn)有異常(如
InvalidOperationException)時(shí) - ? 異常永遠(yuǎn)不會(huì)被
catch區(qū)分處理時(shí)
結(jié)語(yǔ):優(yōu)雅異常的藝術(shù)
異常繼承設(shè)計(jì)看似簡(jiǎn)單,實(shí)則體現(xiàn)了對(duì)系統(tǒng)邊界、錯(cuò)誤傳播和代碼可維護(hù)性的深刻理解。sealed 關(guān)鍵字在這里不是限制,而是保護(hù)——它防止了異常體系的無(wú)序膨脹,確保了每個(gè)異常類型的語(yǔ)義完整性和運(yùn)行時(shí)穩(wěn)定性。
記?。?strong>異常不是業(yè)務(wù)流程,而是業(yè)務(wù)規(guī)則的例外。當(dāng)你的代碼拋出異常時(shí),應(yīng)該讓調(diào)用者無(wú)法忽視,同時(shí)提供足夠的上下文信息。而 sealed 異常,就是這種清晰語(yǔ)義的最佳載體。
討論:你是否有過(guò)因異常繼承層次過(guò)深而導(dǎo)致的調(diào)試噩夢(mèng)?歡迎在評(píng)論區(qū)分享你的經(jīng)歷和見(jiàn)解。
到此這篇關(guān)于C# 異常繼承從設(shè)計(jì)原則到 sealed 關(guān)鍵字的奧秘的文章就介紹到這了,更多相關(guān)C# 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C#實(shí)現(xiàn)把圖片轉(zhuǎn)換成二進(jìn)制以及把二進(jìn)制轉(zhuǎn)換成圖片的方法示例
這篇文章主要介紹了C#實(shí)現(xiàn)把圖片轉(zhuǎn)換成二進(jìn)制以及把二進(jìn)制轉(zhuǎn)換成圖片的方法,結(jié)合具體實(shí)例形式分析了基于C#的圖片與二進(jìn)制相互轉(zhuǎn)換以及圖片保存到數(shù)據(jù)庫(kù)的相關(guān)操作技巧,需要的朋友可以參考下2017-06-06
C#使用System.Threading.Timer實(shí)現(xiàn)計(jì)時(shí)器的示例詳解
以往一般都是用 System.Timers.Timer 來(lái)做計(jì)時(shí)器,其實(shí) System.Threading.Timer 也可以實(shí)現(xiàn)計(jì)時(shí)器功能,下面就跟隨小編一起來(lái)學(xué)習(xí)一下如何使用System.Threading.Timer實(shí)現(xiàn)計(jì)時(shí)器功能吧2024-01-01
C# CancellationToken的使用小結(jié)
在C#中CancellationToken是用于實(shí)現(xiàn)協(xié)作式取消的核心類型,本文就來(lái)詳細(xì)的介紹一下C# CancellationToken的使用小結(jié),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-11-11
C#?wpf使用DockPanel實(shí)現(xiàn)制作截屏框
做桌面客戶端的時(shí)候有時(shí)需要實(shí)現(xiàn)截屏功能,能夠在界面上框選截屏,本文就來(lái)為大家介紹一下wpf如何使用DockPanel制作截屏框吧,感興趣的可以了解下2023-09-09
C#連接SQL?Sever數(shù)據(jù)庫(kù)詳細(xì)圖文教程
C#是Microsoft公司為.NET Framework推出的重量級(jí)語(yǔ)言,和它搭配最完美的數(shù)據(jù)庫(kù)無(wú)疑就是Microsoft SQL Server了,下面這篇文章主要給大家介紹了關(guān)于C#連接SQL?Sever數(shù)據(jù)庫(kù)的詳細(xì)圖文教程,需要的朋友可以參考下2023-06-06
C#代碼實(shí)現(xiàn)根據(jù)模板快速生成Word文檔
在日常開(kāi)發(fā)中,我們經(jīng)常遇到需要批量生成合同、通知書(shū)、報(bào)告等Word文檔的場(chǎng)景,今天小編就來(lái)分享如何使用?Free Spire.Doc for .NET?輕松實(shí)現(xiàn)根據(jù)模板快速生成Word文檔吧2026-04-04
c#中string的特性介紹及注意事項(xiàng)小結(jié)
這篇文章主要給大家介紹了關(guān)于c#中string的特性介紹及注意事項(xiàng)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用c#具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11

