我們?cè)谝话愕慕涌诤瘮?shù)開發(fā)中诱建,為了安全性蝴蜓,我們都需要對(duì)傳入的參數(shù)進(jìn)行驗(yàn)證,確保參數(shù)按照我們所希望的范圍輸入俺猿,如果在范圍之外茎匠,如空值,不符合的類型等等押袍,都應(yīng)該給出異乘忻埃或錯(cuò)誤提示信息。這個(gè)參數(shù)的驗(yàn)證處理有多種方式谊惭,最為簡(jiǎn)單的方式就是使用條件語句對(duì)參數(shù)進(jìn)行判斷汽馋,這樣的判斷代碼雖然容易理解侮东,但比較臃腫,如果對(duì)多個(gè)參數(shù)豹芯、多個(gè)條件進(jìn)行處理悄雅,那么代碼就非常臃腫難以維護(hù)了,本篇隨筆通過分析幾種不同的參數(shù)驗(yàn)證方式铁蹈,最終采用較為優(yōu)雅的方式進(jìn)行處理宽闲。
通常會(huì)規(guī)定類型參數(shù)是否允許為空,如果是字符可能有長(zhǎng)度限制握牧,如果是整數(shù)可能需要判斷范圍容诬,如果是一些特殊的類型比如電話號(hào)碼,郵件地址等沿腰,可能需要使用正則表達(dá)式進(jìn)行判斷览徒。參考隨筆《C# 中參數(shù)驗(yàn)證方式的演變》中文章的介紹,我們對(duì)參數(shù)的驗(yàn)證方式有幾種颂龙。
1吱殉、常規(guī)方式的參數(shù)驗(yàn)證
一般我們就是對(duì)方法的參數(shù)使用條件語句的方式進(jìn)行判斷,如下函數(shù)所示厘托。
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.
}
如果復(fù)雜的參數(shù)校驗(yàn)友雳,那么代碼就比較臃腫
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
}
有時(shí)候?yàn)榱朔奖悖瑫?huì)把參數(shù)校驗(yàn)的方法铅匹,做一個(gè)通用的輔助類進(jìn)行處理押赊,如在我的公用類庫里面提供了一個(gè):參數(shù)驗(yàn)證的通用校驗(yàn)輔助類 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罗丰、基于第三方類庫的驗(yàn)證方式
在GitHub上有一些驗(yàn)證類庫也提供了對(duì)參數(shù)驗(yàn)證的功能神帅,使用起來比較簡(jiǎn)便,采用一種流暢的串聯(lián)寫法萌抵。如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
}
這種書寫方式比較流暢绍填,而且也提供了比較強(qiáng)大的參數(shù)校驗(yàn)方式霎桅,除了可以使用其IsNotNull、IsEmpty等內(nèi)置函數(shù)讨永,也可以使用Evaluate這個(gè)擴(kuò)展判斷非常好的函數(shù)來處理一些自定義的判斷滔驶,應(yīng)該說可以滿足絕大多數(shù)的參數(shù)驗(yàn)證要求了,唯一不好的就是需要使用這個(gè)第三方類庫吧卿闹,有時(shí)候如需擴(kuò)展就麻煩一些揭糕。而且一般來說我們自己有一些公用類庫萝快,如果對(duì)參數(shù)驗(yàn)證也還需要引入一個(gè)類庫,還是比較麻煩一些的(個(gè)人見解)
3著角、Code Contract
Code Contracts 是微軟研究院開發(fā)的一個(gè)編程類庫杠巡,我最早看到是在C# In Depth 的第二版中,當(dāng)時(shí).NET 4.0還沒有出來雇寇,當(dāng)時(shí)是作為一個(gè)第三方類庫存在的氢拥,到了.NET 4.0之后,已經(jīng)加入到了.NET BCL中锨侯,該類存在于System.Diagnostics.Contracts 這個(gè)命名空間中嫩海。
這個(gè)是美其名曰:契約編程
C#代碼契約起源于微軟開發(fā)的一門研究語言Spec#(參見http://mng.bz/4147)。
? 契約工具:包括:ccrewrite(二進(jìn)制重寫器囚痴,基于項(xiàng)目的設(shè)置確保契約得以貫徹執(zhí)行)叁怪、ccrefgen(它生成契約引用集,為客戶端提供契約信息)深滚、cccheck(靜態(tài)檢查器奕谭,確保代碼能在編譯時(shí)滿足要求,而不是僅僅檢查在執(zhí)行時(shí)實(shí)際會(huì)發(fā)生什么)痴荐、ccdocgen(它可以為代碼中指定的契約生成xml文檔)血柳。
? 契約種類:前置條件、后置條件生兆、固定條件难捌、斷言和假設(shè)、舊式契約鸦难。
? 代碼契約工具下載及安裝:下載地址Http://mng.bz/cn2k根吁。(代碼契約工具并不包含在Visual Studio 2010中,但是其核心類型位于mscorlib內(nèi)合蔽。)
? 命名空間:System.Diagnostics.Contracts.Contract
Code Contract 使得.NET 中契約式設(shè)計(jì)和編程變得更加容易击敌,Contract中的這些靜態(tài)方法方法包括
Requires:函數(shù)入口處必須滿足的條件
Ensures:函數(shù)出口處必須滿足的條件
Invariants:所有成員函數(shù)出口處都必須滿足的條件
Assertions:在某一點(diǎn)必須滿足的條件
Assumptions:在某一點(diǎn)必然滿足的條件,用來減少不必要的警告信息
Code Contract 的使用文檔您可以從官網(wǎng)下載到拴事。為了方便使用Visual Studio開發(fā)沃斤。我們可以安裝一個(gè)Code Contracts for .NET 插件。安裝完了之后挤聘,點(diǎn)擊Visual Studio中的項(xiàng)目屬性轰枝,可以看到如下豐富的選擇項(xiàng):
Contract和Debug.Assert有些地方相似:
都提供了運(yùn)行時(shí)支持:這些Contracts都是可以被運(yùn)行的,并且一旦條件不被滿足组去,會(huì)彈出類似Assert的一樣的對(duì)話框報(bào)錯(cuò),如下:
都可以在隨意的在代碼中關(guān)閉打開步淹。
但是Contract有更多和更強(qiáng)大的功能:
Contracts的意圖更加清晰从隆,通過不同的Requires/Ensures等等調(diào)用诚撵,代表不同類型的條件,比單純的Assert更容易理解和進(jìn)行自動(dòng)分析
Contracts的位置更加統(tǒng)一键闺,將3種不同條件都放在代碼的開始處寿烟,而非散見在函數(shù)的開頭和結(jié)尾,便于查找和分析辛燥。
不同的開發(fā)人員筛武、不同的小組、不同的公司挎塌、不同的庫可能都會(huì)有自己的Assert徘六,這就大大增加了自動(dòng)分析的難度,也不利于開發(fā)人員編寫代碼榴都。而Contracts直接被.NET 4.0支持待锈,是統(tǒng)一的。
它提供了靜態(tài)分析支持嘴高,這個(gè)我們可以通過配置面板看到竿音,通過靜態(tài)分析Contracts,靜態(tài)分析工具可以比較容易掌握函數(shù)的各種有關(guān)信息拴驮,甚至可以作為Intellisense
Contract中包含了三個(gè)工具:
ccrewrite, 通過向程序集中些如二進(jìn)制數(shù)據(jù)春瞬,來支持運(yùn)行時(shí)檢測(cè)
cccheck, 運(yùn)行時(shí)檢測(cè)
ccdoc, 將Contract自動(dòng)生成XML文檔
前置條件的處理,如代碼所示套啤。
/// <summary>
/// 實(shí)現(xiàn)“前置條件”的代碼契約
/// </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):表示對(duì)方法輸出的約束:返回值快鱼、out或ref參數(shù)的值,以及任何被改變的狀態(tài)纲岭。Ensures();
/// <summary>
/// 實(shí)現(xiàn)“后置條件”的代碼契約
/// </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之前抹竹,所有的契約都要在真正執(zhí)行方法之前(Assert和Assume除外,下面會(huì)介紹)止潮。
// 2.實(shí)際上Result<int>()僅僅是編譯器知道的”占位符“:在使用的時(shí)候工具知道它代表了”我們將得到那個(gè)返回值“窃判。
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)); // 此結(jié)果表達(dá)式是無法證明真?zhèn)蔚摹? return int.TryParse(text, out value); // 所以此處在編譯前就會(huì)提示錯(cuò)誤信息:Code Contract:ensures unproven: XXXXX
}
這個(gè)代碼契約功能比較強(qiáng)大,不過好像對(duì)于簡(jiǎn)單的參數(shù)校驗(yàn)喇闸,引入這么一個(gè)家伙感覺麻煩袄琳,也不見開發(fā)人員用的有多廣泛,而且還需要提前安裝一個(gè)工具:Code Contracts for .NET燃乍。
因此我也不傾向于使用這個(gè)插件的東西唆樊,因?yàn)榇a要交付客戶使用,要求客戶安裝一個(gè)插件刻蟹,并且打開相關(guān)的代碼契約設(shè)置逗旁,還是比較麻煩,如果沒有打開舆瘪,也不會(huì)告訴客戶代碼編譯出錯(cuò)片效,只是會(huì)在運(yùn)行的時(shí)候不校驗(yàn)方法參數(shù)红伦。
4、使用內(nèi)置的公用類庫處理
基于CuttingEdge.Conditions 的方式淀衣,其實(shí)我們也可以做一個(gè)類似這樣的流暢性寫法的校驗(yàn)處理昙读,而且不需要那么麻煩引入第三方類庫。
例如我們?cè)诠妙悗炖锩嬖黾右粋€(gè)類庫膨桥,如下代碼所示蛮浑。
/// <summary>
/// 參數(shù)驗(yàn)證幫助類,使用擴(kuò)展函數(shù)實(shí)現(xiàn)
/// </summary>
/// <example>
/// eg:
/// ArgumentCheck.Begin().NotNull(sourceArray, "需要操作的數(shù)組").NotNull(addArray, "被添加的數(shù)組");
/// </example>
public static class ArgumentCheck
{
#region Methods
/// <summary>
/// 驗(yàn)證初始化
/// <para>
/// eg:
/// ArgumentCheck.Begin().NotNull(sourceArray, "需要操作的數(shù)組").NotNull(addArray, "被添加的數(shù)組");
/// </para>
/// <para>
/// ArgumentCheck.Begin().NotNullOrEmpty(tableName, "表名").NotNullOrEmpty(primaryKey, "主鍵");</para>
/// <para>
/// ArgumentCheck.Begin().CheckLessThan(percent, "百分比", 100, true);</para>
/// <para>
/// ArgumentCheck.Begin().CheckGreaterThan<int>(pageIndex, "頁索引", 0, false).CheckGreaterThan<int>(pageSize, "頁大小", 0, false);</para>
/// <para>
/// ArgumentCheck.Begin().NotNullOrEmpty(filepath, "文件路徑").IsFilePath(filepath).NotNullOrEmpty(regexString, "正則表達(dá)式");</para>
/// <para>
/// ArgumentCheck.Begin().NotNullOrEmpty(libFilePath, "非托管DLL路徑").IsFilePath(libFilePath).CheckFileExists(libFilePath);</para>
/// <para>
/// ArgumentCheck.Begin().InRange(brightnessValue, 0, 100, "圖片亮度值");</para>
/// <para>
/// ArgumentCheck.Begin().Check<ArgumentNullException>(() => config.HasFile, "config文件不存在只嚣。");</para>
/// <para>
/// ArgumentCheck.Begin().NotNull(serialPort, "串口").Check<ArgumentException>(() => serialPort.IsOpen, "串口尚未打開沮稚!").NotNull(data, "串口發(fā)送數(shù)據(jù)");
/// </para>
/// </summary>
/// <returns>Validation對(duì)象</returns>
public static Validation Begin()
{
return null;
}
/// <summary>
/// 需要驗(yàn)證的正則表達(dá)式
/// </summary>
/// <param name="validation">Validation</param>
/// <param name="checkFactory">委托</param>
/// <param name="argumentName">參數(shù)名稱</param>
/// <returns>Validation對(duì)象</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>
/// 自定義參數(shù)檢查
/// </summary>
/// <typeparam name="TException">泛型</typeparam>
/// <param name="validation">Validation</param>
/// <param name="checkedFactory">委托</param>
/// <param name="message">自定義錯(cuò)誤消息</param>
/// <returns>Validation對(duì)象</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;
}
}
......
上面提供了一個(gè)常規(guī)的檢查和泛型類型檢查的通用方法,我們?nèi)绻枰獙?duì)參數(shù)檢查介牙,如下代碼所示壮虫。
ArgumentCheck.Begin().NotNull(sourceArray, "需要操作的數(shù)組").NotNull(addArray, "被添加的數(shù)組");
而這個(gè)NotNull就是我們根據(jù)上面的定義方法進(jìn)行擴(kuò)展的函數(shù),如下代碼所示环础。
/// <summary>
/// 驗(yàn)證非空
/// </summary>
/// <param name="validation">Validation</param>
/// <param name="data">輸入項(xiàng)</param>
/// <param name="argumentName">參數(shù)名稱</param>
/// <returns>Validation對(duì)象</returns>
public static Validation NotNull(this Validation validation, object data, string argumentName)
{
return Check<ArgumentNullException>(validation, () => (data != null), string.Format(Resource.ParameterCheck_NotNull, argumentName));
}
同樣道理我們可以擴(kuò)展更多的自定義檢查方法囚似,如引入正則表達(dá)式的處理。
ArgumentCheck.Begin().NotNullOrEmpty(libFilePath, "非托管DLL路徑").IsFilePath(libFilePath).CheckFileExists(libFilePath);
它的擴(kuò)展函數(shù)如下所示线得。
/// <summary>
/// 是否是文件路徑
/// </summary>
/// <param name="validation">Validation</param>
/// <param name="data">路徑</param>
/// <returns>Validation對(duì)象</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">當(dāng)文件路徑為null時(shí)</exception>
/// <exception cref="FileNotFoundException">當(dāng)文件路徑不存在時(shí)</exception>
/// <returns>Validation對(duì)象</returns>
public static Validation CheckFileExists(this Validation validation, string filePath)
{
return Check<FileNotFoundException>(validation, () => File.Exists(filePath), string.Format(Resource.ParameterCheck_FileNotExists, filePath));
}
我們可以根據(jù)我們的正則表達(dá)式校驗(yàn)贯钩,封裝更多的函數(shù)進(jìn)行快速使用募狂,如果要自定義的校驗(yàn),那么就使用基礎(chǔ)的Chek函數(shù)即可角雷。
測(cè)試下代碼使用祸穷,如下所示。
/// <summary>
/// 應(yīng)用程序的主入口點(diǎn)勺三。
/// </summary>
[STAThread]
static void Main(string[] args)
{
ArgumentCheck.Begin().NotNull(args, "啟動(dòng)參數(shù)");
string test = null;
ArgumentCheck.Begin().NotNull(test, "測(cè)試參數(shù)").NotEqual(test, "abc", "test");
這個(gè)ArgumentCheck作為公用類庫的一個(gè)類雷滚,因此使用起來不需要再次引入第三方類庫,也能夠?qū)崿F(xiàn)常規(guī)的校驗(yàn)處理吗坚,以及可以擴(kuò)展自定義的參數(shù)校驗(yàn)祈远,同時(shí)也是支持流式的書寫方式,非常方便商源。