下面這幾個,是在實際開發或閱讀中發現的一些問題,有些甚至是有很多年開發人員寫出的代碼,也是很多人經常犯的錯誤。各位可以看看,你有沒有躺着中槍。
第一個,對整型變量進行非null判斷。
// a 是int型 (不是int?) if(a != null){ //操作 }
個人點評:無意義判斷,值類型永遠不可能為null。
第二個,用static來保持頁面回發
static int id; protected void Page_Load(object sender, EventArgs e) { if (Request.QueryString["ID"] != null && Request.QueryString["ID"].ToString() != "") { id = Convert.ToInt32(Request.QueryString["ID"].ToString()); } }
個人點評:這個不解釋,不知道怎么說。但最近還真就遇到了,而且也不是什么小項目,WebForm無服務器控件開發。
第三個,用編程方式綁定數據控件時,數據源為DataSet時判斷null而不判讀DataSet內的Tables數。
DataSet ds = bll.GetList(); if (ds != null) { Repeater1.DataSource = ds; Repeater1.DataBind(); }
個人點評:當bll.GetList()返回的DataSet非null但里面沒有包含數據表時,執行DataBind()方法時會報HttpException異常(IListSource 不包含任何數據源)。正確寫法應該是
DataSet ds = bll.GetList(); if (ds != null && ds.Tables.Count > 0) { Repeater1.DataSource = ds; Repeater1.DataBind(); } //或 DataSet ds = bll.GetList()??new DataSet(); if (ds.Tables.Count > 0) { Repeater1.DataSource = ds; Repeater1.DataBind(); }
第四個,用編程方式綁定數據控件時,數據源為DataTable或List<T>時判斷null。
DataTable dt = bll.GetList(); if (dt!=null) { Repeater1.DataSource = dt; Repeater1.DataBind(); }
個人點評:無意義判斷,下面的寫法沒有任何問題,即使dt=null
DataTable dt = bll.GetList(); Repeater1.DataSource = dt; Repeater1.DataBind();
第五個
Model m = new Model(); m = bll.GetModel(id); m.name;
個人點評:以為只要聲明時不為null,后面就不需要做非空非null判斷了。萬一第二步BLL層返回的model就為null呢?
第六個,在Repeater1_ItemDataBound中寫這樣的代碼
Label lblPMID = (Label)e.Item.FindControl("lblPMID"); if (lblPMID.Text != "") { //操作 }
個人點評:低效,無意義判斷,很可能出現NullReferenceException(未將對象引用設置到對象的實例)異常。
正確寫法:
Label lblPMID = e.Item.FindControl("lblPMID") as Label; if (lblPMID!=null && lblPMID.Text != "") //視里面使用情況決定是否判斷lblPMID.Text為“”或空白 { //操作 }
第七個
string txtName = Request["txtName"] == null ? "" : Request["txtName"].ToString(); string strWhere += "and ID=" + userId + ""; //userId是int if (txtName != "") { strWhere += " and NAME='" + txtName + "'"; } strWhere += " order by id desc"; //項目本身都是采用參數化查詢的,這里是一些暴露給Web層的高級查詢條件。
個人點評:1、值類型和字符串拼接會隱式裝箱,2、SQL注入危險。正確做法是userId.ToString()並且過濾txtName中特殊字符,限制字符串長度。
注意,拼接SQL時過濾字符串並不能完全防止SQL注入,但很多時候在高級查詢時拼接SQL是最簡單也是最方便的,這時候過濾不應該只過濾一些指定的特殊字符,
比如只過濾單引號,等號,大於/小於/等於,空格,括號之類的危險字符。應該對除中文字符、英文字母、和數字外的所有字符全部過濾掉(視情況而定)。
並且嚴格限制字符串長度,一般查詢時輸入的關鍵字不會太長,如果用戶輸入的有空格,就拆分成多個條件。這樣能盡可能的減小SQL注入的機會。
最后,給大家分享幾個小經驗,雖說有些只是語法糖,但卻可以幫助我們更高效編寫或閱讀代碼。
一、引用類型的null值很麻煩,因為類型為null時使用點運算符 (.)會報異常,所以經常要做非null判斷,可以用?? null 合並運算符減少代碼量。例如:
//寫法一 int ID; if (Request.Form["ID"] != null && Request.Form["ID"].ToString() != "") { ID = Convert.ToInt32(Request.Form["ID"].ToString()); } //寫法二 int id; if (int.TryParse(Request.Form["ID"]??"",out id)) { } //方法一 string userName2=string.Empty; if (Session["userName"]!=null) { userName2 = Session["userName"].ToString(); } //方法二 string userName1 = Session["userName"] == null ? "" : Session["userName"].ToString(); //方法三 string userName = (Session["userName"] ?? "").ToString();
二、Web項目中的所有Session或cookie最好統一放到一個類中管理。最重要的目的是把Session中索引名獨立出來管理,也就是除了本類外的所有頁面都不要輸入Session名。
可能用語言表達不夠直白,直接上代碼。
看到很多人是這樣,包括網上流行的一些很常見的輔助類庫。

1 /// <summary> 2 /// Session 操作類 3 /// 1、GetSession(string name)根據session名獲取session對象 4 /// 2、SetSession(string name, object val)設置session 5 /// </summary> 6 public class SessionHelper 7 { 8 /// <summary> 9 /// 根據session名獲取session對象 10 /// </summary> 11 /// <param name="name"></param> 12 /// <returns></returns> 13 public static object GetSession(string name) 14 { 15 return HttpContext.Current.Session[name]; 16 } 17 /// <summary> 18 /// 設置session 19 /// </summary> 20 /// <param name="name">session 名</param> 21 /// <param name="val">session 值</param> 22 public static void SetSession(string name, object val) 23 { 24 HttpContext.Current.Session.Remove(name); 25 HttpContext.Current.Session.Add(name, val); 26 } 27 /// <summary> 28 /// 檢測session是否存在 29 /// </summary> 30 /// <param name="name"></param> 31 /// <returns></returns> 32 public static bool CheckSession(string name) 33 { 34 try 35 { 36 if (GetSession(name) == null) 37 { 38 return false; 39 } 40 else 41 { 42 return true; 43 } 44 } 45 catch 46 { 47 return false; 48 } 49 } 50 }
我想知道,這樣做有什么實際意義么?而且HttpContext.Current還不做null檢查。
項目中還到處都是SessionHelper.SetSession("name"),SessionHelper.GetSession("name")這樣編譯器無法找到具體引用的代碼,當你有很多頁面用到這個會話后,
你再想更改會話名稱或刪除這個會話那將是一場災難,而且這樣的代碼多了還可能出現多個會話重名造成沖突,名稱寫錯造成會話丟失。
要克服以上問題,Web項目的Session會話你應該這樣寫

1 /// <summary> 2 /// 系統中Session會話管理 3 ///<para>注意:該類使用Session,若在一般處理程序中調用該類方法必須實現 IRequiresSessionState 接口</para> 4 /// </summary> 5 public class SessionManager 6 { 7 private const string USER_LOGIN_MARK = "userModel"; //用戶登錄 8 private const string USER_REGISTER_MARK = "registerModel"; //用戶注冊 9 private const string CHECKSUM_MARK = "checksumModel"; //校驗碼 10 private const string CAPTCHA_MARK = "captchaModel"; //驗證碼 11 12 #region 用戶登錄會話 13 /// <summary> 14 /// 添加用戶登錄標識 15 /// </summary> 16 /// <param name="cardModel"></param> 17 internal static void AddUserLoginMark(UserModel model) //UserModel是用戶對象 18 { 19 if (model == null) 20 { 21 throw new ArgumentNullException("參數不能為Null"); 22 } 23 24 HttpContext context = HttpContext.Current; 25 if (context != null) 26 { 27 context.Session[USER_LOGIN_MARK] = model; 28 } 29 } 30 31 /// <summary> 32 /// 移除用戶登錄標識 33 /// </summary> 34 internal static void RemoveUserLoginMark() 35 { 36 HttpContext context = HttpContext.Current; 37 if (context != null) 38 { 39 context.Session.Remove(USER_LOGIN_MARK); 40 } 41 } 42 43 /// <summary> 44 /// 獲取當前登錄用戶對象 45 /// </summary> 46 /// <returns></returns> 47 internal static UserModel GetUserLogin() 48 { 49 HttpContext context = HttpContext.Current; 50 if (context != null) 51 { 52 return context.Session[USER_LOGIN_MARK] as UserModel; 53 } 54 55 return null; 56 } 57 #endregion 58 59 #region 用戶注冊會話 60 /// <summary> 61 /// 添加用戶注冊會話標識 62 /// </summary> 63 /// <param name="cardModel"></param> 64 internal static void AddUserRegisterMark(RegisterModel model) 65 { 66 if (model == null) 67 { 68 throw new ArgumentNullException("參數不能為Null"); 69 } 70 71 HttpContext context = HttpContext.Current; 72 if (context != null) 73 { 74 context.Session[USER_REGISTER_MARK] = model; 75 } 76 } 77 78 /// <summary> 79 /// 移除用戶注冊會話標識 80 /// </summary> 81 internal static void RemoveUserRegisterMark() 82 { 83 HttpContext context = HttpContext.Current; 84 if (context != null) 85 { 86 context.Session.Remove(USER_REGISTER_MARK); 87 } 88 } 89 90 /// <summary> 91 /// 獲取當前注冊會話對象 92 /// </summary> 93 /// <returns></returns> 94 internal static RegisterModel GetUserRegister() 95 { 96 HttpContext context = HttpContext.Current; 97 if (context != null) 98 { 99 return context.Session[USER_REGISTER_MARK] as RegisterModel; 100 } 101 102 return null; 103 } 104 #endregion 105 106 #region 校驗碼會話(手機和郵件) 107 /// <summary> 108 /// 添加一個校驗碼會話 109 /// </summary> 110 internal static void AddChecksumMark(ChecksumModel model) 111 { 112 if (model == null) 113 { 114 throw new ArgumentNullException("參數不能為Null"); 115 } 116 117 HttpContext context = HttpContext.Current; 118 if (context != null) 119 { 120 context.Session[CHECKSUM_MARK] = model; 121 } 122 } 123 124 /// <summary> 125 /// 移除當前用戶的校驗碼會話 126 /// </summary> 127 internal static void RemoveChecksumMark() 128 { 129 HttpContext context = HttpContext.Current; 130 if (context != null) 131 { 132 context.Session.Remove(CHECKSUM_MARK); 133 } 134 } 135 136 /// <summary> 137 /// 獲取當前用戶的校驗碼會話 138 /// </summary> 139 internal static ChecksumModel GetChecksum() 140 { 141 HttpContext context = HttpContext.Current; 142 if (context != null) 143 { 144 return context.Session[CHECKSUM_MARK] as ChecksumModel; 145 } 146 147 return null; 148 } 149 #endregion 150 151 #region 驗證碼會話 152 /// <summary> 153 /// 添加一個驗證碼會話 154 /// </summary> 155 internal static void AddCaptchaMark(string c) 156 { 157 if (c == null) 158 { 159 throw new ArgumentNullException("參數不能為Null"); 160 } 161 162 HttpContext context = HttpContext.Current; 163 if (context != null) 164 { 165 context.Session[CAPTCHA_MARK] = c; 166 } 167 } 168 169 /// <summary> 170 /// 移除當前用戶的驗證碼會話 171 /// </summary> 172 internal static void RemoveCaptchaMark() 173 { 174 HttpContext context = HttpContext.Current; 175 if (context != null) 176 { 177 context.Session.Remove(CAPTCHA_MARK); 178 } 179 } 180 181 /// <summary> 182 /// 獲取當前用戶的驗證碼會話 183 /// </summary> 184 internal static string GetCaptcha() 185 { 186 HttpContext context = HttpContext.Current; 187 if (context != null) 188 { 189 return context.Session[CAPTCHA_MARK] as string; 190 } 191 192 return null; 193 } 194 195 /// <summary> 196 /// 檢查驗證碼是否和服務器端一致(不區分大小寫) 197 /// </summary> 198 /// <param name="code">用戶輸入的驗證碼</param> 199 /// <returns></returns> 200 public static bool ValidateCaptcha(string code) 201 { 202 if (string.IsNullOrWhiteSpace(code)) 203 { 204 throw new ArgumentNullException("參數不能為Null"); 205 } 206 string code2 = GetCaptcha() ?? ""; 207 if (code.ToUpper() == code2.ToUpper()) 208 { 209 return true; 210 } 211 return false; 212 } 213 #endregion 214 }
這樣寫,項目中使用時:
登錄成功就添加會話SessionManager.AddUserLoginMark(當前登錄用戶對象);
頁面登錄檢查時判斷 SessionManager.GetUserLogin();返回是否為null就行。
退出登錄時SessionManager.RemoveUserLoginMark();即可。
這樣就只管調用就行,不用再去管Session名是什么,刪除、更改也更方便,當然也不會出現Session重名現象了(如果這樣都能整成會話重名的話,那你真成人才了)。
當然,這樣寫也不是一點缺點都沒有,和前一種相比,這種方法可能就不能做到一次書寫,到處使用了,需要跟據當前項目具體靈活改動相應代碼,但好處是很明顯的。這樣的方法同樣適用於Cache和Cookie。
三、最好不要用Request[]代替Request.Form[]和Request.QueryString[]。
如果頁面有很多Request.Form[]、Request.QueryString[]、Session[]最好在頁面Page_Load中就把所有值取出來存放在變量中,並轉換成需要的類型。
滿篇的Request.Form[]、Request.QueryString[]、Session[]編譯器沒法檢查[]中的字符串,容易出錯,影響閱讀,還可能同一參數需要多次類型轉換(這一條針對WebFrom無服務器控件開發時特別有用)。
四、盡量使用 as 代替引用類型間轉換(見上面第六個)。
這個大家都知道,但還是發現很多人在用強制轉換,包括一些優秀的開源項目。
五、RegisterClientScriptBlock、RegisterClientScriptInclude、RegisterStartupScript、RegisterOnSubmitStatement、RegisterClientScriptResource等方法要求前台頁面必須有form服務器控件(<form runat="server"></form>)。
也就意味着在WebForm無服務器控件開發時,這幾個沒什么用了(同樣的還有Page.IsPostBack要小心了)。
六、微軟不推薦直接在后台使用Response.Write()輸出JS,並且有的瀏覽器的確會造成頁面變形。
但發現很多人在用,包括一些很優秀的開源項目。我暫時用在前台加入<%= strMsg %>來接收后台傳過來的消息,不知道各位都有什么好的方法。
七、最后向大家分享一段自己寫的小代碼,為Repeater控件添加EmptyDataTemplate模板(EmptyDataTemplate在FooterTemplate之前)。
原理很簡單,默認添加的模板會出現在FooterTemplate之后,在RenderChildren()方法中給它們換下位置就行。
代碼如下:

1 using System; 2 using System.ComponentModel; 3 using System.Web.UI; 4 5 namespace MyRepeater 6 { 7 public class Repeater : System.Web.UI.WebControls.Repeater 8 { 9 [PersistenceMode(PersistenceMode.InnerProperty), Browsable(false), TemplateContainer(typeof(TemplateControl))] 10 public ITemplate EmptyDataTemplate { get; set; } 11 12 protected override void OnDataBinding(EventArgs e) 13 { 14 base.OnDataBinding(e); 15 16 this.Controls.Clear(); 17 this.ClearChildViewState(); 18 19 this.CreateControlHierarchy(true); 20 this.ChildControlsCreated = true; 21 22 if (EmptyDataTemplate != null) 23 { 24 if (this.Items.Count == 0) 25 { 26 EmptyDataTemplate.InstantiateIn(this); 27 } 28 } 29 } 30 31 protected override void RenderChildren(HtmlTextWriter output) 32 { 33 if (HasControls()) 34 { 35 for (int i = 0; i < Controls.Count; i++) 36 { 37 if (this.FooterTemplate != null && this.Items.Count == 0 && EmptyDataTemplate != null) 38 { 39 if (i == Controls.Count - 2) 40 { 41 Controls[i + 1].RenderControl(output); 42 continue; 43 } 44 if (i == Controls.Count - 1) 45 { 46 Controls[i - 1].RenderControl(output); 47 continue; 48 } 49 } 50 Controls[i].RenderControl(output); 51 } 52 } 53 } 54 55 protected override void Render(HtmlTextWriter output) 56 { 57 RenderChildren(output); 58 } 59 } 60 }
暫時能想到的只有這么多,都是自己的經驗之談,當然也屬於一家之言,如果有什么錯誤或不合理的,可以在下邊留言給我或狠狠的踩上幾腳。同時也希望大家能把自己的一些開發經驗或技巧分享出來,供大家學習學習。
2014-01-04補充
錯誤修正
第二段、第七個、為Repeater控件添加EmptyDataTemplate模板會導致Repeater的ItemDataBound事件處理執行兩次。
請注釋掉下面這幾行代碼
//this.Controls.Clear(); //this.ClearChildViewState(); //this.CreateControlHierarchy(true); //this.ChildControlsCreated = true;
本來我的代碼就參考了kdalan的這篇文章,他的文章中本來沒有這幾句的,但在看見msdn中的示例有這幾句,當時也沒多想就加上了,也不一在用,沒出什么毛病。但前兩天在調試的時候發現ItemDataBound事件會執行兩次。為了查找為什么ItemDataBound事件為什么會多次執行,費了好大一番功夫,最后才排確定是this.CreateControlHierarchy(true)這句導致的。因為這幾句是在MSDN上的示例中看到的,所有一直沒懷疑它的正確性,在排錯的過程中放到最后,耽誤不少時間。其實最后想想,也大概知道了其中的原因,.net中的事件可以多次訂,基類和子類都調用this.CreateControlHierarchy(true);會導致多次訂閱ItemDataBound事件。為了證實我的猜想,專門建立了項目,對自定義的Repeater再次繼承重寫,在調用時ItemDataBound事件會被執行三次。測試項目我放到google code上,有興趣的朋友可以下載看看,http://my-repeater.googlecode.com/svn