細說 Request[]與Request.Params[]
https://www.cnblogs.com/fish-li/archive/2011/12/06/2278463.html
細說 Request[]與Request.Params[]
閱讀目錄
- 開始
- 回顧博客原文
- 實現方式分析
- 再談Cookie
- 再談NameValueCollection
- 再談QueryString, Form
- 如何處理沖突
- Request[]還是Request.Params[] ??
今天我來談一談容易被人混淆的二個集合:Request[]與Request.Params[]
這二個集合我在博客【我心目中的Asp.net核心對象】中就提到過它們, 而且還給出了一個示例,並以截圖的形式揭示過它們的差別。但由於那篇博客中有更多有價值的對象要介紹, 因此也就沒有花太多的篇幅著重介紹這二個集合。但我發現,不知道這二個集合差別的人確實太多,以至於我認為很有必要為它們寫個專題來細說它們的差別了。
在ASP.NET編程中,有三個比較常見的來自於客戶端的數據來源:QueryString, Form, Cookie 。 我們可以在HttpRequest中訪問這三大對象, 比如,可以從QueryString中獲取包含在URL中的一些參數, 可以從Form中獲取用戶輸入的表單數據, 可以從Cookie中獲取一些會話狀態以及其它的用戶個性化參數信息。 除了這三大對象,HttpRequest還提供ServerVariables來讓我們獲取一些來自於Web服務器變量。 通常,這4個數據來源都很明確,我想沒人會混淆它們。
一般情況下,如果我們在事先就能明確知道某個參數是來源於哪個集合,那麽直接訪問那個集合,問題也就簡單了。 然而,更常見的數據來源通常只會是QueryString, Form ,而且尤其是當在客戶端使用Jquery的$.ajax()這類技術時, 可以很隨意地將參數放到QueryString或者是Form中,那麽,服務端通常為了也能靈活地應對這一現況, 可以使用Request[]與Request.Params[] 這二種方式來訪問這些來自於用戶提交的數據。 本文的故事也因此而產生了:Request[]與Request.Params[] 有什麽差別??
回顧博客原文
由於【我心目中的Asp.net核心對象】有對它們的一些介紹以及示例截圖, 無奈,有些人可能由於各種原因,沒看到那段文字,這裏我也只好再貼一次了:
這二個屬性都可以讓我們方便地根據一個KEY去【同時搜索】QueryString、Form、Cookies 或 ServerVariables這4個集合。 通常如果請求是用GET方法發出的,那我們一般是訪問QueryString去獲取用戶的數據,如果請求是用POST方法提交的, 我們一般使用Form去訪問用戶提交的表單數據。而使用Params,Item可以讓我們在寫代碼時不必區分是GET還是POST。 這二個屬性唯一不同的是:Item是依次訪問這4個集合,找到就返回結果,而Params是在訪問時,先將4個集合的數據合並到一個新集合(集合不存在時創建), 然後再查找指定的結果。
<body> <p>Item結果:<%= this.ItemValue %></p> <p>Params結果:<%= this.ParamsValue %></p> <hr /> <form action="<%= Request.RawUrl %>" method="post"> <input type="text" name="name" value="123" /> <input type="submit" value="提交" /> </form> </body>
public partial class ShowItem : System.Web.UI.Page { protected string ItemValue; protected string ParamsValue; protected void Page_Load(object sender, EventArgs e) { string[] allkeys = Request.QueryString.AllKeys; if( allkeys.Length == 0 ) Response.Redirect("ShowItem.aspx?name=abc", true); ItemValue = Request["name"]; ParamsValue = Request.Params["name"]; } }
頁面在未提交前瀏覽器的顯示:
點擊提交按鈕後,瀏覽器的顯示:
差別很明顯,我也不多說了。說下我的建議吧:盡量不要使用Params,不光是上面的結果導致的判斷問題, 沒必要多創建一個集合出來吧,而且更糟糕的是寫Cookie後,也會更新集合。
正如我前面所說的客觀原因:由於那篇博客中有更多有價值的對象要介紹,因此也就沒有花太多的篇幅著重介紹這二個集合。 下面,我來仔細地說說它們的差別。
回到頂部實現方式分析
前面的示例中,我演示了在訪問Request[]與Request.Params[] 時得到了不同的結果。為什麽會有不同的結果呢,我想我們還是先去看一下微軟在.net framework中的實現吧。
首先,我們來看一下Request[]的實現,它是一個默認的索引器,實現代碼如下:
public string this[string key] { get { string str = this.QueryString[key]; if( str != null ) { return str; } str = this.Form[key]; if( str != null ) { return str; } HttpCookie cookie = this.Cookies[key]; if( cookie != null ) { return cookie.Value; } str = this.ServerVariables[key]; if( str != null ) { return str; } return null; } }
這段代碼的意思是:根據指定的key,依次訪問QueryString,Form,Cookies,ServerVariables這4個集合,如果在任意一個集合中找到了,就立即返回。
Request.Params[]的實現如下:
public NameValueCollection Params { get { //if (HttpRuntime.HasAspNetHostingPermission(AspNetHostingPermissionLevel.Low)) //{ // return this.GetParams(); //} //return this.GetParamsWithDemand(); // 為了便於理解,我註釋了上面的代碼,其實關鍵還是下面的調用。 return this.GetParams(); } } private NameValueCollection GetParams() { if( this._params == null ) { this._params = new HttpValueCollection(0x40); this.FillInParamsCollection(); this._params.MakeReadOnly(); } return this._params; } private void FillInParamsCollection() { this._params.Add(this.QueryString); this._params.Add(this.Form); this._params.Add(this.Cookies); this._params.Add(this.ServerVariables); }
它的實現方式是:先判斷_params這個Field成員是否為null,如果是,則創建一個集合,並把QueryString,Form,Cookies,ServerVariables這4個集合的數據全部填充進來, 以後的查詢都直接在這個集合中進行。
我們可以看到,這是二個截然不同的實現方式。也就是因為這個原因,在某些特殊情況下訪問它們得到的結果將會不一樣。
不一樣的原因是:Request.Params[]創建了一個新集合,並合並了這4個數據源,遇到同名的key,自然結果就會不同了。
再談Cookie
在博客【我心目中的Asp.net核心對象】中, 說到Request.Params[]時,我簡單地說了一句:而且更糟糕的是寫Cookie後,也會更新集合。 如何理解這句話呢?
我想我們還是來看一下我們是如何寫一個Cookie,並發送到客戶端的吧。下面我就COPY一段 【細說Coookie】中的一段原文吧:
Cookie寫入瀏覽器的過程:我們可以使用如下代碼在Asp.net項目中寫一個Cookie 並發送到客戶端的瀏覽器(為了簡單我沒有設置其它屬性)。
HttpCookie cookie = new HttpCookie("MyCookieName", "string value"); Response.Cookies.Add(cookie);
代碼的關鍵點在於調用了Response.Cookies.Add(),我們來看看它們是如何實現的。首先來看Response.Cookies
public HttpCookieCollection Cookies { get { if (this._cookies == null) { this._cookies = new HttpCookieCollection(this, false); } return this._cookies; } }
再來看HttpCookieCollection這個集合的實現:
public sealed class HttpCookieCollection : NameObjectCollectionBase { public HttpCookieCollection() : base(StringComparer.OrdinalIgnoreCase) { } internal HttpCookieCollection(HttpResponse response, bool readOnly) : base(StringComparer.OrdinalIgnoreCase) { this._response = response; base.IsReadOnly = readOnly; } public void Add(HttpCookie cookie) { if (this._response != null) { this._response.BeforeCookieCollectionChange(); } this.AddCookie(cookie, true); if (this._response != null) { this._response.OnCookieAdd(cookie); } }
註意:由於在HttpResponse中創建HttpCookieCollection時,把HttpResponse對象傳入了,因此,在調用HttpCookieCollection.Add()方法時, 會調用HttpResponse的OnCookieAdd(cookie); 我們接著看:
internal void OnCookieAdd(HttpCookie cookie) { this.Request.AddResponseCookie(cookie); }
又轉到Request對象的調用了,接著看:
internal void AddResponseCookie(HttpCookie cookie) { if (this._cookies != null) { this._cookies.AddCookie(cookie, true); } if (this._params != null) { this._params.MakeReadWrite(); this._params.Add(cookie.Name, cookie.Value); this._params.MakeReadOnly(); } }
從以上的代碼分析中,我們可以看到,我們調用Response.Cookies.Add()時,Cookie也會加到HttpRequest.Cookies中。
而且,如果我們訪問過程Request.Params[],也會加到那個集合中。
再談NameValueCollection
本文一開始的示例中,為什麽代碼 ParamsValue = Request.Params["name"]; 得到的結果是:【abc,123】?
根據前面示例代碼我們可以得知:abc這個值是由QueryString提供的,123這值是由Form提供的,最後由Request.Params[]合並在一起了就變成這個樣子了。 有沒有人想過:為什麽合起來就變成了這個樣子了呢?
要回答這個問題,我們需要回顧一下Params的定義:
public NameValueCollection Params
註意:它的類型是NameValueCollection 。MSDN對這個集合有個簡單的說明:
此集合基於 NameObjectCollectionBase 類。但與 NameObjectCollectionBase 不同,該類在一個鍵下存儲多個字符串值。
為了便於大家更容易理解這個類的工作方式,我畫了一張草圖:
【name】這個key對應差一個ArrayList,而那個ArrayList中,包含了二個字符串:abc 和 123 ,這就是它的工作方式。
既然它能在一個鍵值下存儲多個字符串,那我們就來看一下它到底是如何實現的,直接轉到Add()方法:(註意我在代碼中添加的註釋)
public virtual void Add(string name, string value) { if( base.IsReadOnly ) { throw new NotSupportedException(SR.GetString("CollectionReadOnly")); } this.InvalidateCachedArrays(); // 這是一個關鍵的調用,它調用基類,得到每個name對應的元素, // 而每個name對應的元素是一個ArrayList ArrayList list = (ArrayList)base.BaseGet(name); if( list == null ) { // 如果不存在name對應的元素,則創建ArrayList list = new ArrayList(1); if( value != null ) { // 添加value到ArrayList,它將是第一個值 list.Add(value); } base.BaseAdd(name, list); } else if( value != null ) { // 在原有的ArrayList中繼續添加新的值 list.Add(value); } }
我們再來看一下當我們訪問Params[]這個索引器時,.net framework又是如何實現的:
public string this[string name] { get { return this.Get(name); } set { this.Set(name, value); } } public virtual string Get(string name) { // 根據name得到ArrayList ArrayList list = (ArrayList) base.BaseGet(name); // 將ArrayList變成一個字符串行 return GetAsOneString(list); } private static string GetAsOneString(ArrayList list) { int num = (list != null) ? list.Count : 0; if( num == 1 ) { return (string)list[0]; } if( num <= 1 ) { return null; } StringBuilder builder = new StringBuilder((string)list[0]); for( int i = 1; i < num; i++ ) { builder.Append(‘,‘); // 逗號就來源於此。 builder.Append((string)list[i]); } return builder.ToString(); }
現在,您該明白了為什麽當一個key有多個值時,為什麽會用逗號分開來了吧。
或許,看到這裏,您又有了一個新的想法:對於有多值的情況,還要我來按逗號拆分它們,太麻煩了,有沒有不要拆分的方法呢?
答案是:有的,您可以訪問NameValueCollection的GetValues方法,這個方法的實現如下:
public virtual string[] GetValues(string name) { ArrayList list = (ArrayList)base.BaseGet(name); return GetAsStringArray(list); } private static string[] GetAsStringArray(ArrayList list) { int count = (list != null) ? list.Count : 0; if( count == 0 ) { return null; } string[] array = new string[count]; list.CopyTo(0, array, 0, count); return array; }
我想結果一定是您所期待的,它是一個string[] ,我們可以方便的遍歷它:
string[] array = Request.Params.GetValues("name"); if( array != null ) foreach(string val in array)回到頂部
再談QueryString, Form
前面我解釋了NameValueCollection的工作原理,並揭示了Request.Params["name"]; 得到【abc,123】這個結果的原因。
事實上,這個怪異的結果有時並不只是Params會有,同樣的故事還可能由QueryString, Form這二個對象上演(最終會在Request[]那裏也有體現)。
我還是拿【我心目中的Asp.net核心對象】的示例來說明吧:
protected void Page_Load(object sender, EventArgs e) { string[] allkeys = Request.QueryString.AllKeys; if( allkeys.Length == 0 ) Response.Redirect( Request.RawUrl + "?aa=1&bb=2&cc=3&aa=" + HttpUtility.UrlEncode("5,6,7"), true); StringBuilder sb = new StringBuilder(); foreach( string key in allkeys ) sb.AppendFormat("{0} = {1}<br />", HttpUtility.HtmlEncode(key), HttpUtility.HtmlEncode(Request.QueryString[key])); this.labResult.Text = sb.ToString(); }
頁面最終顯示結果如下(註意鍵值為aa的結果):
示例代碼中,開始部分用於檢查URL是否包含參數,如果沒有,則加入一些參數。寫成這樣的原因是: 第一次訪問這個頁面時,URL中肯定是不包含參數的,為了能演示,所以我就加了一些固定的參數,這樣便於後面的講解。
這個示例也演示了:遇到同名的多個值,用逗號分開的做法,並不是Params才會有的,QueryString也可能會有, 當然,Form也逃不掉這個特性的纏繞,不過,我現在倒想舉個有使用價值的示例。
我有這樣一個錄入界面(左邊),並希望最終的錄入結果以右圖的方式顯示:
我想這個功能的實現並不難,但如何做才是最簡單呢?
下面我貼出我的實現方法,大家看看算不算比較容易:
<tr><td style="vertical-align: top">項目類型</td><td> <% foreach( string pt in AppHelper.ProjectTypes ) { %> <label><input type="checkbox" name="ProjectType" value="<%= pt %>" /><%= pt%></label><br /> <% } %> </td></tr>
註意:所有的checkbox的name都是一樣的。
服務端嘛,我認為沒有必要再貼代碼了,我想您懂的。
在這個示例中,我正好利用了NameValueCollection的這個特點,讓它幫我實現了這個逗號分隔的效果,要不然,我還得自己去做!
回到頂部如何處理沖突
通過前面的一些示例,我們可以看到並非只有Params[]會有沖突,只要類型是NameValueCollection的數據源,都有這個問題。
我要再重申一次:QueryString, Form,Param都有這樣的沖突問題,只是Param需要合並4種數據源,它將沖突的機率變大了。
那麽,我們如何正確的處理這類沖突呢? 還記得我前面提到的NameValueCollection的GetValues方法吧,也只好用它了。(除非你願意自己手工拆分字符串) 然後再用一個循環就可以訪問所有沖突值了,就像下面這樣:
string[] array = Request.Params.GetValues("name"); if( array != null ) foreach(string val in array)
註意:Request[]的返回結果是一個字符串,就不能使用這種方法。但是,它的沖突機率要少很多。
現在,還有個現實的問題:QueryString, Form是最常用的數據源,我們要不要這樣處理它呢?
這的確是個很現實的問題,我認為在回答這個問題前,我們需要分析一下這二個集合出現KEY沖突時是個什麽樣子的。
1. "abc.aspx?id=1 &id=2" 在這URL中,我想問問各位:看到這個URL,您會怎麽想?我認為它是錯的,錯在拼接URL的操作中。 其次,我認為URL的修改或者拼接通常由一個工具類來控制,我們有理由保證它不會出現沖突,畢竟範圍相應較小,我們容易給出這個保證。 因此,我認為,直接訪問QueryString可以忽略這種沖突的可能。
2. 表單數據中name重復的情況。我認為這個集合倒是有可能出現沖突,但也極有可能是我們故意安排的,就像前面的示例一樣。 其次,表單的內容在開發階段相對固定,各個輸入控件的name也是比較清楚的,不存在動態變換的可能,因此, 我認為,直接訪問Form也可以忽略這種沖突的可能。
另一方面,我們平時寫QueryString[], Form[]都太習慣了,這樣的代碼太多了,也不可能改成循環的判斷, 因此,我們只能盡量保證在一個數據源的範圍內,它們是不重復的。 事實上,前二個集合通常僅僅與某一個頁面相關,範圍也相對較小,更容易保證。 因此,如果有了這個保證,在訪問這二類集合時,忽略沖突也是可接受的。
但是,Params需要合並4種數據源,尤其是包含Cookies,ServerVariables這二類對象並非與某個頁面相關,它們是全局的, 因此沖突的可能性會更大,所以,我認為:如果您要訪問Params[],那麽,請改成Params.GetValues() ,因為這樣才更合適。
回到頂部Request[]還是Request.Params[] ??
前面說了這麽多,我想Request[]和Request.Params[]的差別,這次應該是說清楚了,到此也該給個結論了: 到底選擇Request[]還是Request.Params[] ??
我想很多人應該會比較關註這個答案,這裏我也想說說我的觀點,不過,我要說明一點: 本文的所有文字,都只表示我的個人觀點,僅供參考。
我認為:要想清楚地回答這個問題,有必要從二個角度再來觀察這二者:常見用法和設計原因。
1. 常見用法: 我一直認為設計這二個集合是為了方便,讓我們可以不必區分GET, POST而直接得到所需的用戶數據。 如果沒有這二個集合,而我們又需要不區分GET,POST時,顯然就要自己去實現這類的集合, 而在自己實現時,也極有可能是先嘗試訪問QueryString, 如果沒有,再去找Form ...。看到了嗎,這不正是Request[]的實現方式嗎? 也正是基於這個前提,遇到前面那種【abc,123】場景時,Request[]得到的結果或許更符合我們的預期。畢竟在獲取到結果後, 我們會基於結果做一些判斷,比如:name參數可能對應一個數據庫的表字段,用它可以找到一個實際數據行, 如果結果是abc或者是123,這時程序都能處理(都有匹配的記錄),但來個【abc,123】,這個還真沒法處理了。
另一方面,在前面的例子中,我也解釋了這並不是Params[]特有的,QueryString, Form都有這樣的問題,自然地Request[]也有這個問題, 只是由於Params需要合並4類數據源,讓這種沖突的機會更大了。
說到這裏,有必要再來談談前面的幾個例子,【abc,123】中,name在QueryString, Form中重復了,顯然這屬於不合理的設計, 現實情況中,應該是不會產生這類事情的,除非偶然。不過,當偶然的不幸發生時,也正好能體現這二者的差別了。 至於我前面所舉的幾個例子,雖然在現實中不太可能會出現,但我的意圖是在向您展示這些技術的細節差異, 展示一些可能偶然會發生的情況,因此,請不要認為那是個技術誤導。
2. 設計原因:讓我們再從設計嚴謹性這個角度來看待這二者的差別,還是拿【abc,123】這個例子來說吧, Request[]這種依次判斷的方式,顯然會遺漏一些信息,因此,從嚴謹性這個角度來看,Request[]是不完美的。 畢竟,最終用戶會如何以某種想法使用這二個API,沒人知道。微軟是設計平臺的,他們不得不考慮這個問題,不設計這二個集合, 是.net framework的不完善,用錯了,就是我們自己的錯了。
基於以上觀點,我給出我的4點意見:
1. 不建議使用Params[],因為:a. 不希望被偶然情況影響,b. 較高的使用成本。
2. Request[] 使用簡單,適合要求不高的場合:示例代碼。
3. 如果需要兼顧性能和易用性,可以模仿Request[]自行設計。(通常並不需要訪問4個集合)
4. 了解差異,體會細節,有時會發現它還是有利用價值的。
如果,您認為閱讀這篇博客讓您有些收獲,不妨點擊一下右下角的【推薦】按鈕。
如果,您希望更容易地發現我的新博客,不妨點擊一下右下角的【關註 Fish Li】。
因為,我的寫作熱情也離不開您的肯定支持。
感謝您的閱讀,如果您對我的博客所講述的內容有興趣,請繼續關註我的後續博客,我是Fish Li 。
細說 Request[]與Request.Params[]