Visual Studio 2010 中的代碼約定設置
軟件約定稱為代碼約定,通過這一約定可以表示代碼正常工作所需的正式條件。 如果方法未按預期收到數據或生成的數據不符合預期的后置條件,代碼約定將導致代碼引發異常。 有關前置條件和后置條件的概述,您可能需要查看我上個月發表的文章 (msdn.microsoft.com/magazine/gg983479)。
代碼約定是 .NET Framework 4 的一部分,但同樣依賴于 Visual Studio 2010 中的一些功能,例如運行時工具、與 MSBuild 集成以及“項目屬性”框中的屬性頁。 值得注意的是,僅編寫前置條件和后置條件是不夠的。 您還需要為每個項目啟用運行時檢查功能才能使用軟件約定。 您可以通過 Visual Studio 2010 中的“代碼約定”項目屬性頁來完成上述操作。
在本文中,我將討論您可以查看或選擇的各個選項的預定用途,并深入討論使用代碼約定中的參數驗證可以執行的最常見操作的重寫程序工具和實踐。
代碼約定屬性頁
應在所有版本中還是僅在調試版本中實施代碼約定前置條件和后置條件? 實際上,這取決于您對軟件約定概念的理解。 它是設計工作的一部分嗎? 或者,它僅是一種調試措施?
如果它是設計功能,則沒理由剝離發行版中的約定。 如果它僅是一種調試技術,當在發布模式中進行編譯時,您不希望顯示它。
在 .NET Framework 中,代碼約定僅是此框架的一部分并且未融入任何語言。 這樣將更容易在項目中按版本配置它們。 因此,通過軟件約定的 .NET Framework 實現,您可以決定實現約定的合適時間和地點。
圖 1 顯示 Visual Studio 2010 中的屬性頁,通過此頁可以設置軟件約定為應用程序工作的方式。 請注意,此類設置基于項目應用,因此可以根據需要進行調整。
圖 1 Visual Studio 2010 中代碼約定的屬性頁
您可以選擇選項配置(調試、發布等)并僅對該配置應用設置。 這樣,您可以啟用代碼約定用于調試但不用于發布,而且更重要的是,您可以隨時改變決策。
運行時檢查
若要啟用代碼約定,必須選中“執行運行時約定檢查”選項。 如果未選中此選項,則在源代碼中顯示的任何約定說明將可能不會產生任何效果(定義了 DEBUG 符號的任何版本中的 Contract.Assert 和 Contract.Assume 例外,但這不是很重要)。 復選框控制是否在每個編譯步驟結束時觸發重寫程序工具。 重寫程序是一個外部工具,用于對軟件約定進行后處理并修改 MSIL 代碼,以及在合適的位置執行前置條件、后置條件和固定條件檢查。
但是,請注意,如果您具有類似下面這樣的前置條件,則在關閉重寫程序時會得到運行時斷言失敗:
圖 2 顯示了您得到的消息框。
圖 2 代碼需要運行時約定檢查
若要詳細查看運行時檢查的工作方式,請考慮以下代碼:
// Check input values
ValidateOperands(x, y);
ValidateResult();
// Perform the operation
if (x == y)
return 2 * x;
return x + y;
}
約定詳細信息使用 ContractAbbreviator 存儲在 ValidateXxx 方法中,如上個月的專欄所討論。 以下是 ValidateXxx 方法的源代碼:
private void ValidateOperands(Int32 x, Int32 y) {
Contract.Requires(x >= 0 && y >= 0);
}
[ContractAbbreviator]
private void ValidateResult() {
Contract.Ensures(Contract.Result<Int32>() >= 0);
}
如果您使用 Contract.Requires 而不是 Contract.Requires<TException>,則在某個版本中關閉重寫程序時不會出現圖 2 所示的失敗。 圖 2 中的消息框是由 Contract.Requires 的內部實現所致,如下所示:
public static void Requires(bool condition, string userMessage) {
AssertMustUseRewriter(
ContractFailureKind.Precondition, "Requires");
}
public static void Requires<TException>(bool condition)
where TException: Exception {
AssertMustUseRewriter(
ContractFailureKind.Precondition, "Requires<TException>");
}
條件屬性向編譯器指示除非定義了 CONTRACTS_FULL 符號,否則應忽略此類方法調用。 僅當您啟用“執行運行時約定檢查”選項時,才定義此符號。 由于 Contract.Requires<TException> 不是根據條件定義的且缺少該屬性,因此將執行重寫程序檢查,如果禁用運行時約定檢查,則會導致失敗的斷言。
接下來我們將考慮對上述代碼使用重寫程序的效果。 您可以方便地親自驗證我所說的,方法是僅使用斷點并按 Ctrl+F11 在 Visual Studio 2010 中打開反匯編視圖。 圖 3 顯示了在未啟用運行時約定檢查的情況下,逐步查看編譯的 Sum 方法時反匯編視圖的內容。 正如您所看到的,源代碼與您在類中編寫的代碼相同。
圖 3 不執行運行時約定檢查時的反匯編視圖
如果啟用運行時檢查,重寫程序工具將通過編譯器傳遞,返回并編輯 MSIL 代碼。 如果您在啟用代碼約定的情況下逐步執行相同代碼,將看到類似圖 4 的內容。
圖 4 Return 語句后執行的后置條件檢查
明顯的區別是在退出 Sum 方法之前且在 return 語句之后調用 ValidateResult。 您不必是 MSIL 專家就能了解圖 4 中所示代碼的狀況。 在方法開始接受最上面位置的前置條件之前,將對操作數進行驗證。 包含后置條件的代碼將移動到方法的底部,最后一個 return 語句的 MSIL 代碼將也是如此。 更有意思的是,第一個 return 語句(Sum 方法中實現快捷方式的語句)現在只跳到 ValidateResult 開始的地址:
return 2 * x;
00000054 mov eax,dword ptr [ebp-8]
00000057 add eax,eax
00000059 mov dword ptr [ebp-0Ch],eax
0000005c nop
0000005d jmp 0000006B
...
ValidateResult();
0000006b push dword ptr ds:[02C32098h]
...
回到圖 1,請注意“執行運行時約定檢查”復選框旁邊的下拉列表。 您可以通過該列表指示要啟用的軟件約定數目。 存在多個級別:“完全”、“前置和后置”、“前置條件”、“發行版要求”和“無”。
“完全”表示支持所有類型的軟件約定,“無”表示不考慮任何軟件約定。 “前置和后置”排除固定條件。 “前置條件”還排除 Ensure 語句。
“發行版要求”不支持 Contract.Requires 方法,僅允許使用 Requires<TException> 或舊的 If-Then-Throw 格式指定前置條件。
通過項目屬性頁可按項目啟用或禁用運行時檢查,但是如果您只想對代碼的一些部分禁用運行時檢查該怎么辦? 在這種情況下,只需使用 ContractRuntimeIgnored 屬性以編程方式禁用運行時檢查。 但是,最新發行版 (1.4.40307.0) 中增加了新的“跳過限定符”選項,該選項也不允許您執行任何包含對 Contract.ForAll 或 Contract.Exists 的引用的約定。
可以對在 Contract 表達式中使用的成員應用屬性。 如果成員已使用此屬性加以修飾,則顯示該成員的整個 Contract 語句將不會進行運行時檢查。 屬性不會在 Assert 和 Assume 等 Contract 方法中識別。
程序集模式
代碼約定屬性還可用于為約定配置“程序集模式”設置。 此設置是指您打算執行參數驗證的方式。 有兩個可能的選項:“標準約定要求”和“約定引用程序集”。 程序集模式設置可幫助重寫程序等工具在必要時優化代碼并給出合適的警告。 假設您使用程序集模式來聲明您使用代碼約定進行參數驗證的意圖。 程序集模式設置引入了一些必須符合的簡單規則,否則您將收到編譯錯誤。
如果您使用 Requires 和 Requires<T> 方法驗證參數,程序集模式必須設置為“標準約定要求”。 如果您使用任何 If-Then-Throw 語句作為前置條件,則應使用“自定義參數驗證”。 如果您不使用“自定義參數驗證”,該語句將被視為 Requires<T>。 自定義參數驗證的組合以及任何形式的 Requires 語句的顯式使用將引發編譯器錯誤。
使用 Requires 和使用 If-Then-Throw 語句之間有何差別? If-Then-Throw 語句在驗證失敗時始終引發您指示的異常。 在這一點上,它與 Requires 不同,但與 Requires<T> 相似。 純 If-Then-Throw 語句也不會被約定工具(重寫程序和靜態檢查程序)發現,除非您在該語句后調用 EndContractBlock。 使用 EndContractBlock 時,它必須是您在方法中調用的最后一個代碼約定方法。 其后不能執行任何其他代碼約定調用:
throw new ArgumentException();
Contract.EndContractBlock();
此外,Requires 語句是自動繼承的。 除非您也使用 EndContractBlock,否則不會繼承 If-Then-Throw 語句。 在舊模式中,不會繼承 If-Then-Throw 約定。 實際上,您必須手動執行約定繼承。 如果這些工具未檢測到前置條件在重寫和接口實現中重復,將嘗試發出警告。
最后,請注意,不允許 ContractAbbreviator 包含任何 If-Then-Throw 語句,但您可以對該屬性使用約定驗證程序。 縮寫方法只能包含常規 Contract 語句進行參數驗證。
其他設置
在代碼約定屬性頁中,如果選中“執行運行時約定檢查”選項,則將啟用其他一些有用選項。
如果啟用“約定失敗時斷言”選項,則當違反約定時,將導致描述失敗上下文的斷言。 您將看到類似于圖 2 中所示內容的消息框,并且可以選擇一些選項。 例如,您可以再次嘗試附加調試器,中止應用程序或者直接忽略失敗并繼續。
您可能希望僅將此選項用于調試版本,因為顯示的信息對于一般最終用戶來說可能沒有意義。 代碼約定 API 提供了一個集中式異常處理程序用來捕獲任何沖突,并由您判斷錯誤的真正根源。 您收到的信息將區分是前置條件、后置條件還是固定條件失敗,但僅使用布爾表達式并可能使用配置的錯誤消息來描述錯誤特征。 換句話說,從集中式異常處理程序輕松恢復有點難度:
下面是說明處理程序的一些代碼:
Object sender, ContractFailedEventArgs e) {
Console.WriteLine("{0}: {1}; {2}", e.
FailureKind, e.Condition, e.Message);
e.SetHandled();
}
如果要在運行時引發特定異常,則可以使用 Requires<TException>。 如果您打算限制調試版本約定的使用或者如果您不關心異常的實際類型是什么,則可以使用 Requires 和集中式處理程序。 通常這足夠指明發生了錯誤。 例如,許多應用程序在頂層都具有可捕獲各種類型異常并指出如何重新啟動的全能功能。
“僅公共接口約定”選項是指您希望實施代碼約定的位置:每個方法或僅公共方法。 如果選中該選項,重寫程序將忽略代碼約定語句的私有和受保護成員,并僅處理公共成員的約定。
如果您將代碼約定融入您的整體設計從而在任何地方使用代碼約定,選中此選項很有意義。 但是,一旦應用程序做好交付準備,作為一種優化形式,您可以不必檢查內部成員參數,因為沒有外部代碼直接調用這些成員。
將代碼約定限制為程序集的公共接口的想法是否好用,還取決于您編寫代碼的方式。 如果您可以保證公共接口對較低級別發出的任何調用都是正確的,則這是一種優化形式。 如果不能保證,禁用內部方法的約定可導致出現令人厭煩的錯誤。
“調用網站需要檢查”選項提供了另一種優化方案。 假設您正在編寫要由其他程序集中的模塊使用的庫。 出于性能考慮,您禁止對約定進行運行時檢查。 但是,只要您創建了約定引用程序集,就可以使調用方檢查所調用的每個方法的約定。
包含使用代碼約定的類的每個程序集中都可能存在約定引用程序集。 其中包含具有 Contract 語句但沒有其他代碼的父程序集的公共可見接口。 可以從代碼約定屬性頁對程序集的創建進行排序和控制。
旨在調用庫的任何代碼都可能引用約定引用程序集,并且可以通過僅啟用“調用網站需要檢查”選項自動導入約定。 處理調用方代碼時,重寫程序將為隨約定引用程序集一起提供的外部程序集上調用的每個方法導入約定。 這種情況下,將在調用站點(位于調用方側)檢查約定,并保留可對不直接控制的代碼啟用和禁用的可選行為。
總結
代碼約定是 .NET Framework 的一個方面,值得進行更多研究。 我這里只對配置選項進行了簡要的討論,甚至未涉及靜態檢查程序的使用。 代碼約定可幫助您更清楚地設計應用程序以及編寫更簡潔的代碼。
若要了解有關代碼約定的詳細信息,請參見 Melitta Andersen 2009 年 6 月的“CLR 全面透徹解析”專欄 (msdn.microsoft.com/magazine/ee236408) 和 DevLabs Code Contracts 網站 (msdn.microsoft.com/devlabs/dd491992)。 您還可以在 Microsoft Research 的代碼約定網站 (research.microsoft.com/projects/contracts) 上找到有關代碼約定開發的有趣背景信息。