CsharpThinking---代碼契約CodeContract(八)


代碼契約(Code Contract):它並不是語言本身的新功能,而是一些額外的工具,幫助人們控制代碼邊界。

代碼契約之於C#,就相當於詩詞歌賦之於語言。 --- C# in Depth

 

一,概述

  1.1 未引入“代碼契約(特指MS代碼契約)”之前的狀態---“契約”

    • 契約:20世紀80年代,Bertand Meyer在設計Eiffel語言時就將其作為重要的部分。已有大量的計算機科學研究開始探究正式的規范說明和驗證,它允許在編譯時檢查程序的正確性,不過契約的作用還不止於此。

    • 契約編程的核心理念是將API的需求和承諾與實現相分離。

    • 契約約定比文檔約定方式更“同步”一些。 

    • 或使用Debug.Assert方式,但其不能在Release構建時不會捕獲非法參數。

     /************************使用代碼契約方式之前*****************************/
     /// <summary> /// Counts the number of whitespace characters in <paramref name="text"/> /// </summary> /// <param name="text">String to examine. Must not be null.</param> /// <example cref="ArgumentNullException"><paramref name="text"/> is null.</example> /// <returns>The number of whitespace characters</returns> public static int CountWhiteSpace(string text) { if (string.IsNullOrEmpty(text)) throw new ArgumentNullException("text"); return text.Count(char.IsWhiteSpace); }
        /************************使用代碼契約方式之前*****************************/
        /// <summary>
        /// 說明:只能在Debug模式下規范”契約“。
        /// </summary>
        /// <param name="text"></param>
        /// <returns></returns>
        public static int CountStringLength(string text)
        {
            // 命名空間:using System.Diagnostics; 
            Debug.Assert(text != null);
            return text.Length;
        }

 

 1.2 代碼契約

    • C#代碼契約起源於微軟開發的一門研究語言Spec#(參見http://mng.bz/4147)。

    • 契約工具:包括:ccrewrite(二進制重寫器,基於項目的設置確保契約得以貫徹執行)、ccrefgen(它生成契約引用集,為客戶端提供契約信息)、cccheck(靜態檢查器,確保代碼能在編譯時滿足要求,而不是僅僅

          檢查在執行時實際會發生什么)、ccdocgen(它可以為代碼中指定的契約生成xml文檔)。

    • 契約種類:前置條件、后置條件、固定條件、斷言和假設、舊式契約。

      • 代碼契約工具下載及安裝:下載地址Http://mng.bz/cn2k。(代碼契約工具並不包含在Visual Studio 2010中,但是其核心類型位於mscorlib內。)

    • 命名空間:System.Diagnostics.Contracts.Contract

    

    • 代碼契約優勢:編譯前執行檢查(如若設置異常類型使用EnsuresOnThrow<Exception>則會在編譯時拋出異常),這樣比較編譯后檢查有明顯的優勢。

             

 

  1.3 契約種類介紹

    • 前置條件(precondition):是對方法調用者提出的要求,而不是表示普通條件下方法本身的行為。Contract.Requires<T>();

 1         /// <summary>
 2         /// 實現“前置條件”的代碼契約
 3         /// </summary>
 4         /// <param name="text">Input</param>
 5         /// <returns>Output</returns>
 6         public static int CountWhiteSpace(string text)
 7         {
 8             // 命名空間:using System.Diagnostics.Contracts;
 9             Contract.Requires<ArgumentNullException>(text != null, "Paramter:text");// 使用了泛型形式的Requires
10             return text.Count(char.IsWhiteSpace);
11         }

    • 后置條件(postcondition):表示對方法輸出的約束:返回值、out或ref參數的值,以及任何被改變的狀態。Ensures();

        /// <summary>
        /// 實現“前置條件”的代碼契約
        /// </summary>
        /// <param name="text">Input</param>
        /// <returns>Output</returns>
        public static int CountWhiteSpace(string text)
        {
            // 命名空間:using System.Diagnostics.Contracts;
            Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(text), "text"); // 使用了泛型形式的Requires
            Contract.Ensures(Contract.Result<int>() > 0); // 1.方法在return之前,所有的契約都要在真正執行方法之前(Assert和Assume除外,下面會介紹)。
                                                          // 2.實際上Result<int>()僅僅是編譯器知道的”占位符“:在使用的時候工具知道它代表了”我們將得到那個返回值“。
            return text.Count(char.IsWhiteSpace);
        }

        public static bool TryParsePreserveValue(string text, ref int value)
        {
            Contract.Ensures(Contract.Result<bool>() || Contract.OldValue(value) == Contract.ValueAtReturn(out value)); // 此結果表達式是無法證明真偽的。
            return int.TryParse(text, out value); // 所以此處在編譯前就會提示錯誤信息:Code Contract:ensures unproven: XXXXX
        }

    • 固定條件(invariant):它們是只要對象的狀態可見,就必須自始至終遵循的契約。換句話說,在類的公共方法運行時,固定條件可以改變,但在方法的最后,它們仍要滿足契約。

 1         public sealed class CardGame
 2         {
 3             readonly Stack<Card> deck = new Stack<Card>(Card.CreateFullDeck());
 4             readonly Stack<Card> discardPile = new Stack<Card>();
 5             readonly List<Player> players = new List<Player>();
 6 
 7             public void DealCard(Player p)
 8             {
 9                 players.Add(p);
10             }
11 
12             [ContractInvariantMethod]
13             private void ObjectInvarint()
14             {
15                 Contract.Invariant((deck.Count + discardPile.Count + players.Sum(p => p.CardCount)) == Card.FullDeckSize); // 校驗總和是否一致。
16             }
17         }

        特性:1. 固定條件方法是無參數、無返回值、私有的。

           2. 用[ContractInvariantMethod]標簽修飾。

           3. 執行起來代價低廉,不必擔心性能損失。

           4. 如果檢查集合內容還可用Contract.ForAll or Contarct.Exists.

            [ContractInvariantMethod]
            private void InvarintCollection()
            {
                Contract.Invariant(Contract.ForAll(players, item => item.Name != "Stephen")); // 校驗集合。
                Contract.Invariant(!Contract.Exists(players, item => item.Name != "Stephen")); // 與上句作用相同。
            }

    • 斷言和假設 :可檢測在代碼進行到“一半”時發生的事情。

        斷言Assert:靜態檢查器會檢測Assert是否正確,而Assume不會。大多數情況下使用。

        假設Assume:正因為靜態檢查器不會檢查,所以某些情況下需要過濾掉靜態檢查器無法檢驗的東西。

            public static int RollDic(Random rng)
            {
                Contract.Ensures(Contract.Result<int>() >= 2);

                if (rng == null)
                    rng = new Random();
                Contract.Assert(rng != null);

                int firstRoll = rng.Next(1, 7);
                Contract.Assume(firstRoll >= 1);
                Contract.Assume(firstRoll <= 6);

                return firstRoll;
            }

    • 舊式契約:本質上是另一種形式的前置條件。DoNet2.0支持。

            public static int CountSpace(string text)
            {
                if (text == null)
                    throw new ArgumentNullException("text");
                Contract.EndContractBlock(); // 此方法不做任何事情,只是讓二進制編譯器知道,此句以上的部分是契約。

                return text.Count(char.IsWhiteSpace);
            }

 

  二, 使用ccrewrite和ccrefge重寫二進制

      2.1 契約重寫:重寫剛剛獲取的程序集的某些部分,以替換原始的程序集。

      替換的事件序列:

          • 檢查前置條件。

          • 為OldValue方法調用獲取初始的狀態。

          • 執行代碼的功能部分,如Assert

                            • 檢查后置條件

          • 檢查固定條件

 

    2.2 契約繼承(重要)

      • 特性:1. 覆蓋某方法(或實現某個接口方法,規范契約)會繼承該方法中的契約。

         2. 不能再繼承的方法中添加額外的前置條件,但卻可以添加固定條件和后置條件。必須符合Liskov替換原則(里氏代換原則http://zh.wikipedia.org/wiki/Liskov%E4%BB%A3%E6%8F%9B%E5%8E%9F%E5%89%87)。

        • 繼承:使其子類也有父類中的前置條件、后置條件等,作為一種條件約束,但也有其限制,而且繼承很容易被濫用,不如實現接口方式穩妥。

        [Pure]
        public static bool Report(string text)
        {
            Console.WriteLine(text);
            return true;
        }

        public class ContractBase
        {
            public virtual void VirtualMethod(string text)
            {
                Contract.Requires(Report("Base precondition"));
                Contract.Ensures(Report("Base postcondition"));
            }
        }

        public class Derived : ContractBase
        {
            public override void VirtualMethod(string text)
            {
                Contract.Ensures(Report("Dervied postcondition"));
            }
        }

        /* *******************************************************
         * 結果:
         * Base precondition
         * Dervied postcondition
         * Base postconditon
         * 
         * 注:盡管Dervied中覆寫的方法沒有調用base.VirtualMethod(),
         * 契約仍然被執行。
         * *******************************************************/

      • 為接口指定契約:

 1         /*************************************************************
 2          * 說明:
 3          * 1. ICaseConverter與ICaseConverterContracts互為實現(相互反向
 4          *    引用)。
 5          * 2. ICaseConverter只定義接口方法。
 6          * 3. ICaseConverterContracts抽象類中實現接口的約束,內部要定義成
 7          *    私有。
 8          * 4. CurrentCultrueUpperCaseFormatter實現接口ICaseConverter,而
 9          *    與抽象類隔離。
10          * ***********************************************************/
11         /// <summary>
12         /// 接口類
13         /// </summary>
14         [ContractClass(typeof(ICaseConverterContracts))] // 指定契約類
15         public interface ICaseConverter
16         {
17             string Convert(string text);
18         }
19 
20         /// <summary>
21         /// 契約類
22         /// </summary>
23         [ContractClassFor(typeof(ICaseConverter))] // 聲明契約所服務的類型
24         internal abstract class ICaseConverterContracts : ICaseConverter
25         {
26             public string Convert(string text)
27             {
28                 Contract.Requires(!string.IsNullOrEmpty(text)); // 前置條件
29                 Contract.Ensures(Contract.Result<string>() != null); // 后置條件
30                 return default(string);// 如果沒有實現此類,則返回默認值。
31             }
32 
33             private ICaseConverterContracts() { } // 禁止實例化該類
34         }
35 
36         /// <summary>
37         /// 實現類
38         /// </summary>
39         public class CurrentCultrueUpperCaseFormatter : ICaseConverter // 繼承接口中的契約
40         {
41             public string Convert(string text)
42             {
43                 return text.ToUpper(CultureInfo.CurrentCulture); // 實現接口方法。(由二進制重寫器執行檢查)
44             }
45         }

          • 失敗行為

        Contract全局失敗事件:Contract.ContractFailed ,可注冊並捕獲所有Contract失敗事件

        Contract.ContractFailed += new EventHandler<ContractFailedEventArgs>(Contract_ContractFailed);

...
        static void Contract_ContractFailed(object sender, ContractFailedEventArgs e)
        {
            Console.WriteLine("{0}:{1},{2}", e.FailureKind, e.Condition, e.Message);
            e.SetHandled();
        } 

       

  三, 靜態檢查

         • 意義:在編譯時執行檢查,任何錯誤將在Error List中顯示警告信息和錯誤信息。

        static string DontPassMeNull(string input)
        {
            Contract.Requires(input != null);
            Contract.Ensures(Contract.Result<string>() != null);
            return input;
        }

        static string MightReturnNull() 
        {
            return "Not null really";
        }

        /// <summary>
        /// Error list中顯示警告信息
        /// </summary>
        public static void DoTest() 
        {
            DontPassMeNull("definitely okay"); // 總能通過
            DontPassMeNull(MightReturnNull()); // CodeContracts:requires unproven:input != null 無法證明
            DontPassMeNull(null); // 提示錯誤信息
        }

      • 靜態檢查選項:在屬性頁中選擇Implicit Non-null Obligations選項可執行前置空引用的檢查,Implicit Array Bounds Obligations選項可檢查數組是否越界。

      • 有選擇性的執行檢查

      1. BaseLine(基線)方法:

        在屬性頁中,選中:

       則,在程序跟目錄會生成baseline.xml文件,包含所有警告信息。

      將錯誤導出到文件中有利於分析錯誤。

    2. 特性控制檢查:

      [assembly: ContractVerification(false)]

        [ContractVerification(false)]

 

  四,后記(契約實戰)

      4.1 契約是一種穩固的保障:它不僅意味着在滿足前置條件時,代碼將以特定的方式運行,還意味着在不滿足的時候,就不會繼續執行。

    4.2 不要考慮為不易變類型設置固定條件。如果某些情況不會改變類型的狀態,那它也肯定不會破壞固定條件。相反,如果在構造時有必須遵循的規則,那么也應該是前置條件。

 

更多關於“代碼契約”內容請閱讀《C# in Depth》

官方文檔:http://download.csdn.net/detail/cuiyansong/5580307

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM