dotNET:怎樣處理程序中的異常(理論篇)?

平時(shí)在軟件開發(fā)的過程中,首先是要保證功能可以正常運(yùn)行,滿足業(yè)務(wù)需求,除此之外,還需要考慮代碼在異常的時(shí)候怎么處理,讓程序能夠健壯地運(yùn)行。正確合理地處理異常可以減少程序的 Bug、保證代碼質(zhì)量,當(dāng)然也不是一件很容易的事。
在日常工作中我們排查錯(cuò)誤時(shí)經(jīng)常會(huì)遇到這樣一些問題,如果沒有,說明你做的還不錯(cuò)了:
想通過日志的方式分析錯(cuò)誤原因,發(fā)現(xiàn)日志記錄不完整;
找到錯(cuò)誤日志了,記錄的是“未將對象引用設(shè)置到對象的實(shí)例”,也知道代碼行數(shù),然而這一行上有多個(gè)引用類型的對象,還是不知道真實(shí)原因;
問題是偶發(fā)的,無法重現(xiàn)。
最終需要還原數(shù)據(jù)庫進(jìn)行單步調(diào)試才能解決問題,然而:
客戶的數(shù)據(jù)庫涉密,不能提供;
客戶的數(shù)據(jù)庫運(yùn)行多年,數(shù)據(jù)量很大,無法快速備份還原;
如果是互聯(lián)網(wǎng) Saas 應(yīng)用,更是難于將庫拿到本地進(jìn)行調(diào)試。
所以需要在代碼層面、在日志層面來進(jìn)行優(yōu)化來達(dá)到可以快速定位問題的目的。
dotNET 經(jīng)典錯(cuò)誤

上面這張圖,經(jīng)歷過 dotNET Framework 時(shí)代的程序員應(yīng)該都不陌生,這就是經(jīng)典的「黃頁」和經(jīng)典的 「未將對象引用設(shè)置到對象的實(shí)例」錯(cuò)誤。
首先這個(gè)錯(cuò)誤顯示非常不友好,除了讓人知道這個(gè)是 dotNET 開發(fā)的,別無他用,另外這個(gè)錯(cuò)誤提示對排查錯(cuò)誤也沒有幫助,只知道對象為 null 了,但原因是什么并不知道,只能猜,能不能猜中就得看運(yùn)氣了。
正確的錯(cuò)誤處理思路
一個(gè)系統(tǒng)一般有兩類人使用,普通用戶和系統(tǒng)管理員。不管是普通用戶還是系統(tǒng)管理員,在操作系統(tǒng)時(shí)都期望所有的操作是有反饋的,要么正常返回想要的結(jié)果,要么給出友好的錯(cuò)誤提示,能夠指引進(jìn)行下一步操作。
當(dāng)出現(xiàn)異常時(shí),可以導(dǎo)向一個(gè)專屬類型的錯(cuò)誤提示頁面,也可以以模態(tài)的方式彈出錯(cuò)誤提示,內(nèi)容包含:
錯(cuò)誤提示,例如:系統(tǒng)異常,請聯(lián)系管理員,撥打 xxx 、保存失敗,請聯(lián)系管理員;
全局錯(cuò)誤碼,下面會(huì)講到;
異常編碼,可以根據(jù)此編碼在后臺的日志記錄快速查詢,異常編碼使用日期加流水號即可,建議不要使用 Guid,曾經(jīng)被非技術(shù)人員當(dāng)成是亂碼。
如果是系統(tǒng)管理員使用的功能,將真實(shí)錯(cuò)誤原因顯示在錯(cuò)誤提示中,我認(rèn)為也是可以的。
全局錯(cuò)誤碼
設(shè)置全局錯(cuò)誤碼,可以讓管理員在收到反饋的錯(cuò)誤時(shí)能快速地根據(jù)錯(cuò)誤碼進(jìn)行問題的定位和找到解決方法。所以需要有公開的全局錯(cuò)誤碼文檔,記錄錯(cuò)誤的原因和解決方案參考。
大類上可以分為 4xx 和 5xx,4xx 表示前端的參數(shù)問題、驗(yàn)證問題等,5xx 表示后端的邏輯問題。
在 5xx 類型中可以再進(jìn)行細(xì)分,例如:
500100:表示數(shù)據(jù)庫操作相關(guān)問題
500200:表示列表展示相關(guān)問題
等等
異常處理的一些原則
1、在方法中不要返回錯(cuò)誤碼,因?yàn)殄e(cuò)誤碼的信息太單一;
2、拋異常時(shí)選擇具體的異常類型,不要直接拋出 System.Exception ;
3、錯(cuò)誤信息目的是為了讓開發(fā)人員可以定位問題和解決問題,而不是給最終用戶看,給前端用戶看的信息要友好易懂;
4、不能吞異常,比如 catch 異常后不做任何處理,如果有些資源需要清理,可以使用 try…finally 或者使用 using ;
5、只有當(dāng)你知道怎么樣從異常中恢復(fù)時(shí),才需要去捕獲異常,在執(zhí)行一些操作時(shí),我們可能知道出現(xiàn)錯(cuò)誤的原因,但無法恢復(fù),這時(shí)不要去捕獲異常。
在方法中怎樣處理異常?
一個(gè)方法中有三個(gè)部分:參數(shù)、業(yè)務(wù)邏輯和返回值
參數(shù)
引用類型的參數(shù),在方法的開始一定要做非空判斷,判斷后是拋異常還是繼續(xù)下面的邏輯這個(gè)要根據(jù)具體情況來定:
如果參數(shù)為 null 時(shí)會(huì)對后續(xù)的業(yè)務(wù)有影響,就應(yīng)該拋出異常;
如果我們判斷 null 后能做一些初始化處理,能讓程序繼續(xù)正常運(yùn)行,而且保證業(yè)務(wù)也是正確的,就不必拋異常。
業(yè)務(wù)邏輯
業(yè)務(wù)邏輯的部分分為三種情況:
在方法內(nèi)部調(diào)用其他類型的一個(gè)方法,比如
var user= userService.GetUser();對 user 的判斷,當(dāng)為 null 時(shí)是否拋異常,跟上面參數(shù)的邏輯一致;多個(gè)邏輯組合到一起進(jìn)行判斷后,如果不能滿足下一步的輸入,應(yīng)該拋出異常;
對于更低一層的調(diào)用,有時(shí)會(huì)進(jìn)行異常的捕獲,當(dāng)捕獲到異常后,應(yīng)該要拋出符合當(dāng)前上下文的專有異常信息,更利于定位問題。
返回值
一個(gè)方法的返回值可以返回值類型,如 string、int、bool ,也可以返回引用類型,如返回一個(gè) User 對象,不管是返回什么類型,原則是一樣的,都需要更具上下文來進(jìn)行判斷。
有個(gè) GetUser 方法來獲取用戶對象 ,如果根據(jù) Id 沒有找到用戶,可以直接返回 null ,而不是返回一個(gè)空的 User 對象,如果返回空對象,程序不會(huì)出錯(cuò),但前端展示卻沒有數(shù)據(jù),就搞不清是沒找到用戶,還是找到了但沒值;返回 null,可以由上層來決定怎么來處理。
再有個(gè) GetUserList 方法根據(jù)條件獲取用戶集合,如果根據(jù)搜索條件沒有找到符合的用戶,可以返回空對象 List,而不是返回 null 。
對于值類型也是一樣,要看上下文,比如 C# 中用來查找字符在一個(gè)字符串中的索引位置的函數(shù) IndexOf ,返回的是 int 類型,當(dāng)找不到的時(shí)候返回的是 -1 ,而不是 null 。
最后
好的異常處理可以使我們的程序更加的健壯,也能在出現(xiàn)問題時(shí)更好的定位和排查問題,本文的內(nèi)容偏理論,下一篇以代碼示例的方式來進(jìn)行演練下。
希望本文對您有所幫助。
