| C++ 傳統做法 | .NET 4.0 Contract |
巨量 #ifdef CONTRACT_FULL 混雜業務碼 | 乾淨的 Contract.Requires(...) 呼叫 |
| 專案間巨集定義不一,無標準 | BCL 內建,跨專案一致 |
| 僅運行期斷言 (assert) | **靜態驗證 + 運行期 + 文檔生成** |
| 效能顧慮難以條件編譯移除 | Release 可自動移除非 Precondition 契約 |
核心 API 速查表
| 契約類型 | 方法 | 用途 | 運行期行為 |
| 斷言 | Contract.Assert(cond, msg?) | 程式中任意點必真 | Debug.Assert |
| 假設 | Contract.Assume(cond, msg?) | 靜態驗證器假設為真 | 同 Assert |
| 前置條件 | Contract.Requires(cond, msg?) | 參數驗證、狀態檢查 | Debug.Assert + Environment.FastFail |
| 前置條件(永不移除) | Contract.RequiresAlways(...) | 公開 API 參數防禦 | 所有建置皆檢查 |
| 後置條件 | Contract.Ensures(cond) | 正常返回保證 | 同 Precondition |
| 例外時後置條件 | Contract.EnsuresOnThrows<TEx>(cond) | 拋出特定例外時保證 | 同 Precondition |
| 不變量 | Contract.Invariant(cond) | 物件永恆條件 | 每 public 方法結束檢查 |
實戰範例:有理數類別
using System.Diagnostics.Contracts;
public class RationalNumber
{
private int _numerator;
private int _denominator;
public RationalNumber(int numerator, int denominator)
{
// Precondition: 分母不可為 0
Contract.Requires(denominator != 0, "Denominator cannot be zero.");
_numerator = numerator;
_denominator = denominator;
}
public int Denominator
{
get
{
// Postcondition: 取得值時保證非 0
Contract.Ensures(Contract.Result<int>() != 0);
return _denominator;
}
}
// Object Invariant: 物件內部永遠滿足分母非 0
[ContractInvariantMethod]
private void ObjectInvariant()
{
Contract.Invariant(_denominator != 0);
}
}
後置條件進階特殊方法
| 方法 | 用途 | 範例 |
Contract.Result<T>() | 存取回傳值 | Contract.Ensures(Contract.Result<int>() > 0) |
Contract.OldValue<T>(expr) | 存取方法入口時的值 | Contract.Ensures(Contract.OldValue(_count) < _count) |
Contract.ValueAtReturn<T>(out param) | 存取 out 參數返回值 | Contract.Ensures(Contract.ValueAtReturn(out x) == 3) |
Contract.ForAll(coll, pred) | 集合所有元素滿足謂詞 | Contract.Requires(Contract.ForAll(items, i => i != null)) |
Contract.Exists(coll, pred) | 集合存在元素滿足謂詞 | Contract.Ensures(Contract.Exists(list, x => x.IsValid)) |
OldValue 使用限制 (重要)
- 僅能用於
Ensures / EnsuresOnThrows 內。
- 不可巢狀:
OldValue(OldValue(x)) ❌
- 不可存取回傳值:
OldValue(Contract.Result<int>()) ❌
- 不可存取
out 參數:OldValue(outParam) ❌
ForAll/Exists 謂詞內不可存取謂詞參數:ForAll(0, n, i => OldValue(arr[i])) ✅ (索引器參數);ForAll(0, n, i => OldValue(i)) ❌
介面與抽象方法契約
介面/抽象方法無方法本體,需**契約類別**配合雙屬性:
[ContractClass(typeof(IAccountContract))]
public interface IAccount
{
int Balance { get; }
void Deposit(int amount);
}
[ContractClassFor(typeof(IAccount))]
sealed class IAccountContract : IAccount
{
public int Balance
{
get
{
Contract.Ensures(Contract.Result<int>() >= 0);
return default(int); // 必須提供 dummy return
}
}
public void Deposit(int amount)
{
Contract.Requires(amount > 0);
// 若需呼叫介面其他成員:
IAccount self = this;
Contract.Requires(self.Balance < 10000);
}
}
契約屬性總覽
| 屬性 | 目標 | 用途 |
[ContractClass] | 介面/抽象類別 | 指向契約類別 |
[ContractClassFor] | 契約類別 | 指向被契約介面 |
[ContractInvariantMethod] | 方法 | 標記不變量方法 |
[Pure] | 方法/委派 | 無副作用,可用於契約中 |
[ContractVerification] | 組件/類別/成員 | (false) 停用靜態驗證 |
[ContractPublicPropertyName] | 私有欄位 | 契約中以公開屬性名稱參考 |
靜態驗證器 (Static Checker) - 關鍵價值
- **編譯期發現 Bug**:未執行程式即偵測 Precondition 違規、Invariant 破壞。
- 整合 Visual Studio 建置流程 (需安裝 Code Contracts 擴充套件)。
- 輸出警告/錯誤列表,可點擊跳轉原始碼。
- 支援
[ContractVerification(false)] 逐步導入舊專案。
已知限制 (NET 4.0 版本)
| 限制 | 說明 |
| 值類型 (struct) 的 Invariant 被忽略 | CLR 不支援 struct 完成項/攔截 |
靜態驗證不支援 ForAll/Exists | 運行期仍檢查 |
C# Iterator (yield return) 契約問題 | 編譯器產生狀態機,Precondition 位置錯位 |
| 非公用方法不檢查 Invariant | 設計決策:僅驗證公開介面 |
效能與部署策略
- Debug 建置:啟用所有契約檢查 (Pre/Post/Invariant/Assert)。
- Release 建置:預設僅保留
Requires (前置條件);可組態保留更多。
- 公開 API:使用
RequiresAlways 確保參數驗證永不移除。
- 內部邏輯:用
Requires,Release 可移除提升效能。
與 D 語言契約比較
D 語言原生 in/out/invariant 區塊,語法更優雅、編譯器深度整合。.NET 契約雖為庫級實現,但配合**靜態驗證器**已達實用水準,且優勢在於**生態整合** (VS、CI/CD、文檔生成)。
結語
契約式程式設計將「文檔中的規範」搬進「可驗證的程式碼」。.NET 4.0 提供成熟工具鏈:Contract.Requires/Ensures/Invariant + 靜態驗證器。建議:**公開 API 必加 Precondition、核心領域模型加 Invariant、關鍵演算法加 Postcondition**,逐步建立「有契約的代碼庫」,大幅降低防禦性程式碼、提早攔截邏輯錯誤、改善團隊溝通精度。
- 上一篇文章: LINQ to SQL N 層架構查詢 (上):自訂實體回傳與資料傳輸載體設計
- 下一篇文章: 分散式事務深度解析 (三):System.Transactions 依賴事務與 TransactionScope 完整實戰
Copyright ©
程式碼之家 All Rights Reserved