標籤:style http java color 使用 strong
何謂異常
很多人在討論異常的時候很模糊,彷彿所謂異常就是try{}catch{},異常就是Exception,非常的片面,所以導致異常影響效能,XXXX……等很多奇怪的言論,所以在此我意在對異常正名。以下,我將異常這個很寬泛,容易被曲解的詞進行嚴格的劃分。
異常機制
所謂異常機制也就是指的語言平台支援異常這種錯誤處理模式的機制,比如c#裡的Exception對象,try{}catch{}finally{}結構,throw拋出異常的語句,等等,均為c#語言裡對異常機制的實現。
異常機制是隨著語言而存在的,一種語言既然支援異常機制,那麼異常就是不可迴避的,哪怕你自己不throw異常,你所使用的系統類別,也會拋出異常給你,所以說討論在系統裡用不用異常是非常可笑的事情。異常機制就像系統的後門,當一個過程執行中系統出錯的時候,或者你認為系統不正常的時候,就把當時的情況拍個快照產生異常對象,通過特殊的通道通知調用這個過程的方法。
異常對象
異常對象是異常機制中用來描述異常,記錄錯誤資訊,可以說是錯誤發生是的快照,就跟你開快車闖紅燈被天眼拍到的照片差不多。
拋出異常
所謂拋出異常就是異常機制中通知調用的方法本方法出錯了的手段。而拋出異常也是很多人詬病異常對效能影響的地方,因為系統效能的開銷都是由拋出異常所引起的。但是拋出異常也無法避免,因為這是系統本身的特點,你不拋出異常,系統自己也會拋出,一旦系統拋出了異常對象,你怎麼來避免這個效能的開銷?沒轍
比如方法A調用了方法B,方法B訪問資料庫,出錯了,那麼B就會拋出異常,而A就需要catch這個異常來處理。
拋出異常有兩種形式,一種是系統拋出的異常,一種是我們自己認為在處理一段邏輯的時候當邏輯錯誤,就可以通過拋出一個異常對象,來通知調用這個方法的方法,這裡出錯了。
系統拋出異常
系統拋出的異常對編程人員來說是透明的,也就是我們不需要關心系統是如何得知出錯了的,系統類別庫一旦出錯就會將異常對象拋給我們調用它的方法,因為系統本身並不知道要如何處理這個錯誤。
使用者拋出異常
使用者拋出異常通常在自己寫類庫,定義一套API給別人使用的時候用到,這個時候我們並不知道要如何處理這些邏輯錯誤,所以就需要交給知道如何處理邏輯錯誤的方法去處理。
在C#裡通過定義了throw關鍵字來拋出異常對象,這裡除了拋出新建立的異常對象,也可以通過將已經catch到的異常重新拋出。如果不知道如何處理這個異常,重新拋出異常是很好的習慣,因為既然已經拋出了異常,那麼效能已經損失了,所以也不在乎多這麼一丁點來更好的挽回錯誤的結果。
處理異常
所有的異常必須得到妥善的處理,也就是說你必須處理所有的異常。因為系統出錯就拋異常,所以這個是c#骨子裡的東西,無法避免,所以,系統裡到處都會充斥著try{}catch{}。
但是try{}catch{}本身並不會影響系統的效能,所以在沒有異常發生的時候try{}catch{}是不會讓你系統變慢的,而一旦發生系統異常,你不處理系統就崩潰掉了,你到底是願意系統慢一點點然後處理掉這個錯誤呢還是願意系統崩潰掉呢?
對於處理異常這一點其實我覺得java那種比較嚴格的,要求嚴格聲明並處理所有異常的方式比較好,能夠強制讓你重視起異常這回事來,免得系統崩潰掉才想起自己沒處理這個異常。
如何處理
如何正確的處理異常?這是個很多c#程式員都沒最終搞明白的話題,很多人所瞭解的也無非就是不要用異常處理當商務邏輯,但是何謂用異常來處理邏輯就不得而知了。這裡我來詳細說明一下,什麼地方要用異常,什麼地方不要用。
首先有一個前提,異常機制是一個由底向上的冒泡的過程,所以正常的邏輯是,異常由底層拋出,由高層來處理。
處理異常正確的例子
要編寫健壯的引用程式,首先要保證必須處理所有的系統異常,也就是調用.NET類庫的方法的時候,這個方法可能拋出的異常
例:
我們可以看到,GetResponse方法會拋出兩個異常 InvalidOperationException和WebException。那麼我們就需要將調用的代碼所在的catch單獨處理這兩個異常
如果你在這個方法中不知道應該如何處理這個異常,比如這個方法不上不下,和表現層離了好幾層,而又需要在表現層通知使用者或者由上層業務來決定是通知使用者還是悄悄的進村,又或者是悄悄的重新嘗試一次,那麼自己不能決定的事情就要拋給上層去處理。
有人可能會說.NET可以統一處理異常的,不過我不推薦那種大而全的處理方式,不夠細緻,很多時候會被奇怪的錯誤搞得你很追查錯誤的本源。且比如說遇到網路問題重試就沒法處理。
異常必須就近處理,這樣才能方便追查,而且注意那個“xxx方法出錯了”那個地方,很重要,這個描述可以讓你在系統排錯的時候少走很多彎路,盡量寫詳細點。
如果你的代碼是給別的程式員使用,而不是和終端使用者直接互動,那麼除了處理所有系統拋出的異常以外,還需要用異常來驗證過濾入口參數。比如,假設你要給其他程式員提供一個將使用者物件插入資料庫的方法:
public void InsertUser(User user) { if(user==null) { throw new ArgumentNullException("參數user為null"); } //調用Orm }
這裡我們驗證了入口參數,並儘早的拋出了異常,因為這裡拋出的異常和不處理等db操作拋出異常,肯定是這裏手動拋出的開銷更小。這裡有一個原則就是,如果這個參數會造成底層代碼直接出錯,那麼就就近處理它,而不要放任其在底層造成系統異常的拋出。當然還有一個原則就是不要在這裡判斷商務邏輯,比如上面的例子就不要在這裡驗證User的屬性的數值是不是合法之類的。
處理異常錯誤的例子
1:用異常驗證使用者輸入
使用者輸入的合法性驗證是屬於商務邏輯的一部分,絕對不要用異常去處理,注意,是使用者輸入,所以這個經驗僅限於表現層邏輯
典型的錯誤1:
try{ int i=int.Parse(textBox1.Text);}catch(Exception ex){ alert(“不要輸入非數字”);}
典型錯誤2:
void ValidateInput(int i){ if(i<0&&i>100) { throw new Exception("輸入資料範圍錯誤"); }}
以上兩種錯誤都是錯誤使用異常的典型,
2:將異常延遲到底層
這一點我們在正確的例子裡提到過
典型錯誤3:
try { string name=Request.QueryString["xx"]; List<User> userls=User.QueryUserByName(name); } catch(SqlException ex) { }
這個錯誤在於完全不驗證使用者輸入而直接把資料的驗證拋向資料庫,等待資料庫報錯來判斷使用者輸入的正確性,這個是非常致命的錯誤,很多注入漏洞都是由此產生的。
3:完全不用異常機制
產生這個錯誤肯定是一個非常腦殘的決定造成的。不過很多時候某些不瞭解異常機制的人,由於對異常的效能開銷的恐懼感,經常會做出這麼腦殘的決定。
典型錯誤4:
public bool InsertUser(User user,ref int errcode) { if(user==null) { errcode=110;//參數為空白錯誤的代碼 return false; } //調用Orm }
感覺就是一夜回到瞭解放前,效能倒是高了,但是系統異常怎麼辦呢?一旦資料庫出錯就只等著系統崩潰了。某些有經驗的說我會把下面的try{}catch{}起來,不過那不是脫了褲子放屁麼,異常都拋出來了,開銷已經產生了,結果換來的是犧牲了異常對象的豐富資訊而換來了畸形的系統邏輯。效能也沒得到提高。
異常對效能的影響
異常機制是C#的特徵,因此決定了你不可能逃避,所以討論異常給你帶來了多大開銷都是扯淡,沒有必要的研究,從根本上無法解決問題。我們應該弄清楚的是,異常的拋出給系統帶來什麼樣的影響,如何在保證系統健壯性的基礎上減小不必要的效能消耗。
1.異常的效能開銷隨著調用棧的深度增加而增大。
對比測試:
測試代碼1
Stopwatch sw = new Stopwatch(); sw.Start(); for (int i = 0; i < 10; i++) { try { throw new Exception("test"); } catch { } } sw.Stop(); MessageBox.Show(sw.ElapsedMilliseconds + "ms");
測試結果:
測試代碼2
Stopwatch sw = new Stopwatch(); sw.Start(); for (int i = 0; i < 10; i++) { try { System.IO.File.OpenRead("c:\\不存在的txt.txt"); } catch { } } sw.Stop(); MessageBox.Show(sw.ElapsedMilliseconds + "ms");
測試結果
小結:由此我們可以發現隨著調用棧的深入,效能開銷也越大,所以異常應該儘早拋出。
2.對輸入的資料應該在商務邏輯中嚴格檢查。
3.try{}catch{}不會造成任何的系統開銷,造成系統開銷的是throw 拋出異常,這是再三強調的了
諸如:
這種言論就純屬腦殘了。
結論
異常機制是C#內建的錯誤處理機制,你無法避免它,唯一正確的道路是學會如何正確使用
希望大家正確使用異常,好好學習天天向上