歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux編程 >> Linux編程 >> C#的未來:方法契約

C#的未來:方法契約

日期:2017/3/1 9:29:12   编辑:Linux編程

  近些年來,開發者可以通過代碼契約(Code Contracts)這個研究性項目獲得添加方法級別契約的能力,但這種方式存在許多問題,它所使用的命令式語法相當冗長,並且通過工具提供的語法支持也很差。無論是開發類庫或是應用程序,要完整的利用這一契約特性,必須要運行某種編譯後指令。總的來說,這是一個有趣的項目,但要真正變得實用,還需要第一等的編譯器與語法的支持。

  第 119 號提議——方法契約旨在提供這種支持。這一語法要求在方法簽名與方法體之間定義前置與後置條件,與泛型的約束寫法類似。下面這個示例展示了該語法的表現形式:

public int Insert (T item, int index) 
    requires index >= 0 && index <= Count 
ensures return >= 0 && return < Count 
{ … }

  這條提議中共包含三個新的關鍵字。“requires”開頭的語句負責處理前置條件,多數情況下將用於檢查參數,但理論上也可以用於檢查對象本身的狀態。“ensures”開頭的語句用於設定後置條件,它重用了“return”關鍵字,以指代該方法調用的返回結果。

  快速失敗還是拋出異常

  類似於代碼契約,這條提議最初的目的也是產生快速失敗。這是一種相當激進的強制契約形式,任何對契約的違反都會立即導致應用的崩潰。在這種模型下,傾向於拋出異常的開發者不得不手動地進行標注:

public int Insert (T item, int index) 
    requires index >= 0 && index <= Count 
        else throw new ArgumentOutOfRangeException (nameof (index)) 
    ensures return >= 0 && return < Count 
{ … }

  對於該提議的這一部分,人們的反對相當激烈。

  Nathan Jervis 寫道:

你了解程序中哪些部分不會受到影響,並且能夠安全地繼續執行下去。可能在某些情況下你會編寫某些極其關鍵的代碼,或許你會希望實現快速失敗,但我不認為了解你的程序中哪一部分產生錯誤是一件不可能的事。

以立即干掉整個進程的方式作為正確的處理行為,這種假設讓人覺得十分可笑。如果微軟的 Word 有某個代碼錯誤,在將文件保存到某個網絡路徑時產生了一個 bug,你會希望它立即干掉整個應用嗎?不,你會希望它能夠將文件暫存在某個臨時路徑下,然後為用戶顯示一條錯誤信息,記錄這個問題,最後在下次加載文件時讓用戶選擇嘗試恢復它。

  HaloFour 也表示附和:

我認為由於參數校驗違反契約而導致整個進程崩潰絕對是一種愚蠢的做法。這種實現方式肯定會導致這一特性將無人問津,除非某人有意要寫一些難懂的代碼。這一特性中的這方面目的在於參數校驗,而程序完全能夠以某種形式從參數校驗錯誤的情況下恢復正常運行。而且坦白地說,即使程序無法恢復正常,調用者也可以決定不要捕獲這個異常。這也是我所看到在 .NET 或其它任何語言中實現的代碼契約的工作方式。

  David Nelson 則提到了代碼契約的經歷:

如果你之前曾經使用過代碼契約進行開發,那麼你一定注意到它在是否應當采取快速失敗這一方式上曾經導致大量的爭論。代碼契約團隊進行了幾個月(甚至幾年?)的努力,試圖說服整個社區:快速失敗才是正確的做法,可最終他們還是失敗了。我深切感受到了那個極具誤解性的決定所帶來的後果,這讓我無法擁抱這一提議。我曾經是快速失敗這一荒謬的方式最堅定的反對者,而且我還會繼續反對下去。

  稍後,他明確地列舉了快速失敗方式所導致的問題:

1) 你怎樣記錄錯誤日志?使用 Watson 顯然不能滿足需求,絕大多數的 .NET 應用程序都不會使用它,因為它提供的信息非常有限並且難以理解,訪問這些信息也很困難。我所看到過的每個 .NET 應用程序都會生成獨有的錯誤日志。

2) 只因為某個用戶在某種極端情況下遇到了一個無害的邏輯 bug,就要讓為全球幾百萬用戶提供服務的某個生產環境中的 web 服務器掛掉,這種做法真的合適嗎?

3) 如果某個單元測試違反了契約的話該怎麼辦?讓單元測試執行器崩潰嗎?

4) 如果一個程序的錯誤會導致進程的崩潰,為什麼在 .NET 中其它的錯誤情況下會拋出異常?NullReferenceException 又為什麼還會存在,難道不應當直接干掉進程嗎?為什麼在 JIT 過程中編譯某個方法失敗時(這很明顯意味著存在某個比違反契約嚴重得多的問題)會拋出異常,而不是直接干掉進程?

  Aaron Dandy 則希望能夠得到兩種選擇:

我當然會使用快速失敗,但我只想在我私人的工作中使用它,在公共項目中我還是希望使用異常。如果調用我代碼的用戶決定用大量的異常去喂飽異常這個怪獸(即選擇使用異常),那也是他們自己的選擇,他們(同時也隱含了他們代碼的用戶)也需要為這一決定買單。

  HaloFour 對以下觀點表示同意:

我更希望方法契約能夠拋出異常(至少是對於 requires 語句來說),然後添加一個語言關鍵字斷言,在某種條件未滿足的情況下會快速失敗。

  異常類型

  這條提議中比較容易接受的部分是 Argument 異常,編譯器會將某個簡單的 requires 語句轉化為某個 ArgumentNullException 或 ArgumentOutOfRange 異常。如果 requires 語句檢查的內容是對象的狀態,那麼它可以拋出一個 InvalidOperationException 異常。但如果該語句同時檢查參數與對象狀態呢?這種情況下要決定拋出何種異常會成為一個相當復雜的問題。

  另外一個問題在於 ObjectDisposedException,因為沒有什麼標准方式能夠表現一個被回收的對象。因此只能采取一些寬松的約定,檢查是否存在某個叫做_disposed 或m_IsDisposed 之類名稱的布爾型字段。這一點的重要性在於,InvalidOperationException 異常一般來說能夠通過改變對象狀態的方式進行恢復,而 ObjectDisposedException 永遠做不到這一點。

  另一方面,需要通過某種異常表示 ensures 語句出錯。與 requires 語句不同,在 ensures 契約中的錯誤總是意味著在方法內部存在 bug。

  本地化

  假設我們采取了某種基於異常的方式,那麼接下來的問題就是本地化了。對於基本的參數檢查來說,編譯器可以簡單地生成包含英文文本的參數異常。但如果這些異常信息需要本地化為其它語言呢?如果選擇使用簡化的語法,就不會明確地拋出某個參數異常。這種情況下,或者需要通過某種渠道添加本地化的信息,或者不得不使用冗長的語法以顯示本地化異常信息。

  枚舉與契約

  目前為止所討論的契約都是一種附加條件,而在 Fabian Schmied 所提出的提議中,編譯器將允許省略那些絕對不會命中的 return 語句。

public string GetText (MyEnum myEnum) 
requires defined (myEnum) 
{ 
    switch (myEnum) 
    { 
        case One: return "Single"; 
        case Two: return "Pair"; 
        case Three: return "Triple"; 
    }    
    // 所有分支情況都已涵蓋,因此即使省略了 return 語句也不會產生錯誤。 
}

  英文原文:C# Futures: Method Contracts

Copyright © Linux教程網 All Rights Reserved