使用优雅方式对参数验证进行处理

我们在一般的接口函数开发中,为了安全性,我们都需要对传入的参数进行验证,确保参数按照我们所希望的范围输入,如果在范围之外,如空值,不符合的类型等等,都应该给出异常或错误提示信息。这个参数的验证处理有多种方式,最为简单的方式就是使用条件语句对参数进行判断,这样的判断代码虽然容易理解,但比较臃肿,如果对多个参数、多个条件进行处理,那么代码就非常臃肿难以维护了,本篇随笔通过分析几种不同的参数验证方式,最终采用较为优雅的方式进行处理。
通常会规定类型参数是否允许为空,如果是字符可能有长度限制,如果是整数可能需要判断范围,如果是一些特殊的类型比如电话号码,邮件地址等,可能需要使用正则表达式进行判断。参考随笔《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中的这些静态方法方法包括
Requires:函数入口处必须满足的条件
Ensures:函数出口处必须满足的条件
Invariants:所有成员函数出口处都必须满足的条件
Assertions:在某一点必须满足的条件
Assumptions:在某一点必然满足的条件,用来减少不必要的警告信息

Code Contract 的使用文档您可以从官网下载到。为了方便使用Visual Studio开发。我们可以安装一个Code Contracts for .NET 插件。安装完了之后,点击Visual Studio中的项目属性,可以看到如下丰富的选择项:


Contract和Debug.Assert有些地方相似:
都提供了运行时支持:这些Contracts都是可以被运行的,并且一旦条件不被满足,会弹出类似Assert的一样的对话框报错,如下:
都可以在随意的在代码中关闭打开。

但是Contract有更多和更强大的功能:
Contracts的意图更加清晰,通过不同的Requires/Ensures等等调用,代表不同类型的条件,比单纯的Assert更容易理解和进行自动分析
Contracts的位置更加统一,将3种不同条件都放在代码的开始处,而非散见在函数的开头和结尾,便于查找和分析。
不同的开发人员、不同的小组、不同的公司、不同的库可能都会有自己的Assert,这就大大增加了自动分析的难度,也不利于开发人员编写代码。而Contracts直接被.NET 4.0支持,是统一的。
它提供了静态分析支持,这个我们可以通过配置面板看到,通过静态分析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作为公用类库的一个类,因此使用起来不需要再次引入第三方类库,也能够实现常规的校验处理,以及可以扩展自定义的参数校验,同时也是支持流式的书写方式,非常方便。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,230评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,261评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,089评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,542评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,542评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,544评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,922评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,578评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,816评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,576评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,658评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,359评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,920评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,859评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,381评论 2 342

推荐阅读更多精彩内容