使用優雅方式對參數驗證進行處理


我們在一般的接口函數開發中,為了安全性,我們都需要對傳入的參數進行驗證,確保參數按照我們所希望的范圍輸入,如果在范圍之外,如空值,不符合的類型等等,都應該給出異常或錯誤提示信息。這個參數的驗證處理有多種方式,最為簡單的方式就是使用條件語句對參數進行判斷,這樣的判斷代碼雖然容易理解,但比較臃腫,如果對多個參數、多個條件進行處理,那么代碼就非常臃腫難以維護了,本篇隨筆通過分析幾種不同的參數驗證方式,最終采用較為優雅的方式進行處理。

通常會規定類型參數是否允許為空,如果是字符可能有長度限制,如果是整數可能需要判斷范圍,如果是一些特殊的類型比如電話號碼,郵件地址等,可能需要使用正則表達式進行判斷。參考隨筆《C# 中參數驗證方式的演變》中文章的介紹,我們對參數的驗證方式有幾種。

1、常規方式的參數驗證

一般我們就是對方法的參數使用條件語句的方式進行判斷,如下函數所示。

public bool Register(string name, int age)
{
    if (string.IsNullOrEmpty(name))
    {
        throw new ArgumentException("name should not be empty", "name");
    }
    if (age < 10 || age > 70)
    {
        throw new ArgumentException("the age must between 10 and 70","age");
    }

    //insert into db
}

或者

public void Initialize(string name, int id)
{
    if (string.IsNullOrEmpty(value))
        throw new ArgumentException("name");
    if (id < 0) 
        throw new ArgumentOutOfRangeException("id");
    // Do some work here.
}

如果復雜的參數校驗,那么代碼就比較臃腫

void TheOldFashionWay(int id, IEnumerable<int> col, 
    DayOfWeek day)
{
    if (id < 1)
    {
        throw new ArgumentOutOfRangeException("id", 
            String.Format("id should be greater " +
            "than 0. The actual value is {0}.", id));
    }

    if (col == null)
    {
        throw new ArgumentNullException("col",
            "collection should not be empty");
    }

    if (col.Count() == 0)
    {
        throw new ArgumentException(
            "collection should not be empty", "col");
    }

    if (day >= DayOfWeek.Monday &&
        day <= DayOfWeek.Friday)
    {
        throw new InvalidEnumArgumentException(
            String.Format("day should be between " +
            "Monday and Friday. The actual value " +
            "is {0}.", day));
    }

    // Do method work
}

有時候為了方便,會把參數校驗的方法,做一個通用的輔助類進行處理,如在我的公用類庫里面提供了一個:參數驗證的通用校驗輔助類 ArgumentValidation,使用如下代碼所示。

     public class TranContext:IDisposable   
     {   
         private readonly TranSetting setting=null;   
         private IBuilder builder=null;   
         private ILog log=null;   
         private ManuSetting section=null;   
         public event EndReportEventHandler EndReport;   
         public TranContext()   
         {   
        }   
        public TranContext(TranSetting setting)   
        {   
            ArgumentValidation.CheckForNullReference (setting,"TranSetting");   
            this.setting =setting;   
        }   
        public TranContext(string key,string askFileName,string operation)   
        {   
            ArgumentValidation.CheckForEmptyString (key,"key");   
            ArgumentValidation.CheckForEmptyString (askFileName,"askFileName");   
            ArgumentValidation.CheckForEmptyString (operation,"operation");   
            setting=new TranSetting (this,key,askFileName,operation);   
        }  

但是這樣的方式還是不夠完美,不夠流暢。

2、基於第三方類庫的驗證方式

在GitHub上有一些驗證類庫也提供了對參數驗證的功能,使用起來比較簡便,采用一種流暢的串聯寫法。如CuttingEdge.Conditions等。CuttingEdge.Condition 里面的例子代碼我們來看看。

public ICollection GetData(Nullable<int> id, string xml, IEnumerable<int> col)
{
    // Check all preconditions:
    Condition.Requires(id, "id")
        .IsNotNull()          // throws ArgumentNullException on failure
        .IsInRange(1, 999)    // ArgumentOutOfRangeException on failure
        .IsNotEqualTo(128);   // throws ArgumentException on failure

    Condition.Requires(xml, "xml")
        .StartsWith("<data>") // throws ArgumentException on failure
        .EndsWith("</data>") // throws ArgumentException on failure
        .Evaluate(xml.Contains("abc") || xml.Contains("cba")); // arg ex

    Condition.Requires(col, "col")
        .IsNotNull()          // throws ArgumentNullException on failure
        .IsEmpty()            // throws ArgumentException on failure
        .Evaluate(c => c.Contains(id.Value) || c.Contains(0)); // arg ex

    // Do some work

    // Example: Call a method that should not return null
    object result = BuildResults(xml, col);

    // Check all postconditions:
    Condition.Ensures(result, "result")
        .IsOfType(typeof(ICollection)); // throws PostconditionException on failure

    return (ICollection)result;
}
    
public static int[] Multiply(int[] left, int[] right)
{
    Condition.Requires(left, "left").IsNotNull();
    
    // You can add an optional description to each check
    Condition.Requires(right, "right")
        .IsNotNull()
        .HasLength(left.Length, "left and right should have the same length");
    
    // Do multiplication
}

這種書寫方式比較流暢,而且也提供了比較強大的參數校驗方式,除了可以使用其IsNotNull、IsEmpty等內置函數,也可以使用Evaluate這個擴展判斷非常好的函數來處理一些自定義的判斷,應該說可以滿足絕大多數的參數驗證要求了,唯一不好的就是需要使用這個第三方類庫吧,有時候如需擴展就麻煩一些。而且一般來說我們自己有一些公用類庫,如果對參數驗證也還需要引入一個類庫,還是比較麻煩一些的(個人見解)

 

3、Code Contract

Code Contracts 是微軟研究院開發的一個編程類庫,我最早看到是在C# In Depth 的第二版中,當時.NET 4.0還沒有出來,當時是作為一個第三方類庫存在的,到了.NET 4.0之后,已經加入到了.NET BCL中,該類存在於System.Diagnostics.Contracts 這個命名空間中。

這個是美其名曰:契約編程

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

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

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

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

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

Code Contract 使得.NET 中契約式設計和編程變得更加容易,Contract中的這些靜態方法方法包括

  1. Requires:函數入口處必須滿足的條件
  2. Ensures:函數出口處必須滿足的條件
  3. Invariants:所有成員函數出口處都必須滿足的條件
  4. Assertions:在某一點必須滿足的條件
  5. Assumptions:在某一點必然滿足的條件,用來減少不必要的警告信息

Code Contract 的使用文檔您可以從官網下載到。為了方便使用Visual Studio開發。我們可以安裝一個Code Contracts for .NET 插件。安裝完了之后,點擊Visual Studio中的項目屬性,可以看到如下豐富的選擇項:

Contract和Debug.Assert有些地方相似:

  1. 都提供了運行時支持:這些Contracts都是可以被運行的,並且一旦條件不被滿足,會彈出類似Assert的一樣的對話框報錯,如下:
  2. 都可以在隨意的在代碼中關閉打開。

但是Contract有更多和更強大的功能:

  1. Contracts的意圖更加清晰,通過不同的Requires/Ensures等等調用,代表不同類型的條件,比單純的Assert更容易理解和進行自動分析
  2. Contracts的位置更加統一,將3種不同條件都放在代碼的開始處,而非散見在函數的開頭和結尾,便於查找和分析。
  3. 不同的開發人員、不同的小組、不同的公司、不同的庫可能都會有自己的Assert,這就大大增加了自動分析的難度,也不利於開發人員編寫代碼。而Contracts直接被.NET 4.0支持,是統一的。
  4. 它提供了靜態分析支持,這個我們可以通過配置面板看到,通過靜態分析Contracts,靜態分析工具可以比較容易掌握函數的各種有關信息,甚至可以作為Intellisense

Contract中包含了三個工具:

  • ccrewrite, 通過向程序集中些如二進制數據,來支持運行時檢測
  • cccheck, 運行時檢測
  • ccdoc, 將Contract自動生成XML文檔

前置條件的處理,如代碼所示。

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

后置條件(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
        }

這個代碼契約功能比較強大,不過好像對於簡單的參數校驗,引入這么一個家伙感覺麻煩,也不見開發人員用的有多廣泛,而且還需要提前安裝一個工具:Code Contracts for .NET

因此我也不傾向於使用這個插件的東西,因為代碼要交付客戶使用,要求客戶安裝一個插件,並且打開相關的代碼契約設置,還是比較麻煩,如果沒有打開,也不會告訴客戶代碼編譯出錯,只是會在運行的時候不校驗方法參數。

 

4、使用內置的公用類庫處理

基於CuttingEdge.Conditions 的方式,其實我們也可以做一個類似這樣的流暢性寫法的校驗處理,而且不需要那么麻煩引入第三方類庫。

例如我們在公用類庫里面增加一個類庫,如下代碼所示。

    /// <summary>
    /// 參數驗證幫助類,使用擴展函數實現
    /// </summary>
    /// <example>
    /// eg:
    /// ArgumentCheck.Begin().NotNull(sourceArray, "需要操作的數組").NotNull(addArray, "被添加的數組");
    /// </example>
    public static class ArgumentCheck
    {
        #region Methods
        
        /// <summary>
        /// 驗證初始化
        /// <para>
        /// eg:
        /// ArgumentCheck.Begin().NotNull(sourceArray, "需要操作的數組").NotNull(addArray, "被添加的數組");
        /// </para>
        /// <para>
        /// ArgumentCheck.Begin().NotNullOrEmpty(tableName, "表名").NotNullOrEmpty(primaryKey, "主鍵");</para>
        /// <para>
        /// ArgumentCheck.Begin().CheckLessThan(percent, "百分比", 100, true);</para>
        /// <para>
        /// ArgumentCheck.Begin().CheckGreaterThan&lt;int&gt;(pageIndex, "頁索引", 0, false).CheckGreaterThan&lt;int&gt;(pageSize, "頁大小", 0, false);</para>
        /// <para>
        /// ArgumentCheck.Begin().NotNullOrEmpty(filepath, "文件路徑").IsFilePath(filepath).NotNullOrEmpty(regexString, "正則表達式");</para>
        /// <para>
        /// ArgumentCheck.Begin().NotNullOrEmpty(libFilePath, "非托管DLL路徑").IsFilePath(libFilePath).CheckFileExists(libFilePath);</para>
        /// <para>
        /// ArgumentCheck.Begin().InRange(brightnessValue, 0, 100, "圖片亮度值");</para>
        /// <para>
        /// ArgumentCheck.Begin().Check&lt;ArgumentNullException&gt;(() => config.HasFile, "config文件不存在。");</para>
        /// <para>
        /// ArgumentCheck.Begin().NotNull(serialPort, "串口").Check&lt;ArgumentException&gt;(() => serialPort.IsOpen, "串口尚未打開!").NotNull(data, "串口發送數據");
        /// </para>
        /// </summary>
        /// <returns>Validation對象</returns>
        public static Validation Begin()
        {
            return null;
        }
        
        /// <summary>
        /// 需要驗證的正則表達式
        /// </summary>
        /// <param name="validation">Validation</param>
        /// <param name="checkFactory">委托</param>
        /// <param name="argumentName">參數名稱</param>
        /// <returns>Validation對象</returns>
        public static Validation Check(this Validation validation, Func<bool> checkFactory, string argumentName)
        {
            return Check<ArgumentException>(validation, checkFactory, string.Format(Resource.ParameterCheck_Match2, argumentName));
        }
        
        /// <summary>
        /// 自定義參數檢查
        /// </summary>
        /// <typeparam name="TException">泛型</typeparam>
        /// <param name="validation">Validation</param>
        /// <param name="checkedFactory">委托</param>
        /// <param name="message">自定義錯誤消息</param>
        /// <returns>Validation對象</returns>
        public static Validation Check<TException>(this Validation validation, Func<bool> checkedFactory, string message)
        where TException : Exception
        {
            if(checkedFactory())
            {
                return validation ?? new Validation()
                {
                    IsValid = true
                };
            }
            else
            {
                TException _exception = (TException)Activator.CreateInstance(typeof(TException), message);
                throw _exception;
            }
        }
......

上面提供了一個常規的檢查和泛型類型檢查的通用方法,我們如果需要對參數檢查,如下代碼所示。

ArgumentCheck.Begin().NotNull(sourceArray, "需要操作的數組").NotNull(addArray, "被添加的數組");

而這個NotNull就是我們根據上面的定義方法進行擴展的函數,如下代碼所示。

        /// <summary>
        /// 驗證非空
        /// </summary>
        /// <param name="validation">Validation</param>
        /// <param name="data">輸入項</param>
        /// <param name="argumentName">參數名稱</param>
        /// <returns>Validation對象</returns>
        public static Validation NotNull(this Validation validation, object data, string argumentName)
        {
            return Check<ArgumentNullException>(validation, () => (data != null), string.Format(Resource.ParameterCheck_NotNull, argumentName));
        }

同樣道理我們可以擴展更多的自定義檢查方法,如引入正則表達式的處理。

ArgumentCheck.Begin().NotNullOrEmpty(libFilePath, "非托管DLL路徑").IsFilePath(libFilePath).CheckFileExists(libFilePath);

它的擴展函數如下所示。

        /// <summary>
        /// 是否是文件路徑
        /// </summary>
        /// <param name="validation">Validation</param>
        /// <param name="data">路徑</param>
        /// <returns>Validation對象</returns>
        public static Validation IsFilePath(this Validation validation, string data)
        {
            return Check<ArgumentException>(validation, () => ValidateUtil.IsFilePath(data), string.Format(Resource.ParameterCheck_IsFilePath, data));
        }

        /// <summary>
        /// 檢查指定路徑的文件必須存在,否則拋出<see cref="FileNotFoundException"/>異常。
        /// </summary>
        /// <param name="validation">Validation</param>
        /// <param name="filePath">文件路徑</param>
        /// <exception cref="ArgumentNullException">當文件路徑為null時</exception>
        /// <exception cref="FileNotFoundException">當文件路徑不存在時</exception>
        /// <returns>Validation對象</returns>
        public static Validation CheckFileExists(this Validation validation, string filePath)
        {
            return Check<FileNotFoundException>(validation, () => File.Exists(filePath), string.Format(Resource.ParameterCheck_FileNotExists, filePath));
        }

我們可以根據我們的正則表達式校驗,封裝更多的函數進行快速使用,如果要自定義的校驗,那么就使用基礎的Chek函數即可。

測試下代碼使用,如下所示。

        /// <summary>
        /// 應用程序的主入口點。
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {
            ArgumentCheck.Begin().NotNull(args, "啟動參數");
            string test = null;
            ArgumentCheck.Begin().NotNull(test, "測試參數").NotEqual(test, "abc", "test");

這個ArgumentCheck作為公用類庫的一個類,因此使用起來不需要再次引入第三方類庫,也能夠實現常規的校驗處理,以及可以擴展自定義的參數校驗,同時也是支持流式的書寫方式,非常方便。 


免責聲明!

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



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