在日常編程實踐中,斷言與異常的界限不是很明顯,這也使得它們常常沒有被正確的使用。我也在不斷的與這個模糊的怪獸搏斗,僅寫此文和大家分享一下我的個人看法。我想我們還可以從很多角度來區別斷言和異常的使用場景,歡迎大家的意見和建議。
異常的使用場景:用於捕獲外部的可能錯誤
斷言的使用場景:用於捕獲內部的不可能錯誤
我們可以先仔細分析一下我們在.net中已經存在的異常。
System.IO.FileLoadException
SqlException
IOException
ServerException
首先,我們先不將它們看成異常,因為我們現在還沒有在異常和斷言之間划清界限,我們先將它們看成錯誤。
當我們在編碼的第一現場考慮到可能會出現文件加載的錯誤或者服務器錯誤后,我們的第一直覺是這不是我們代碼的問題,這是我們代碼之外的問題。
例如下面這段代碼
public void WriteSnapShot(string fileName, IEnumerable<DbItem> items) { string format = "{0}\t{1}\t{2}\t{3}\t{4}\t{5}"; using (FileStream fs = new FileStream(fileName, FileMode.Create)) { using (StreamWriter sw = new StreamWriter(fs, Encoding.Unicode)) { ... foreach (var item in items) { sw.WriteLine(string.Format(format, new object[]{ item.dealMan, item.version, item.priority, item.bugStatus, item.bugNum, item.description})); } sw.Flush(); } } }
上面的代碼在寫入文件,很顯然會導致IOException。稍微有經驗的程序員都會考慮到IO上可能出問題,那我們應該如何處理這個問題呢?在這個上下文中,我們別無它法,只能讓這個錯誤繼續往上拋,通知上面一層的調用者,有一個錯誤發生了,至於上一層調用者會如何處理,不是這個函數要考慮的問題。但在這個函數中,要記得一點,將當前函數中所占用的資源釋放了。因此,當我們不能控制的外部錯誤出現時,我們可以將其作為異常往上拋,這時,我們該使用異常。
現在再來看看斷言,我們還是以下面的一段代碼為例子。
1 public Entities.SimpleBugInfo GetSimpleBugInfo(string bugNum) 2 { 3 4 var selector = DependencyFactory.Resolve<ISelector>(); 5 6 var list = selector.Return<Entities.SimpleBugInfo>( 7 reader => new Entities.SimpleBugInfo 8 { 9 bugNum = reader["bugNum"].ToString(), 10 dealMan = reader["dealMan"].ToString(), 11 description = reader["description"].ToString(), 12 size = Convert.ToInt32(reader["size"]), 13 fired = Convert.ToInt32(reader["fired"]), 14 }, 15 "select * from bugInfo", 16 new WhereClause(bugNum, "bugNum")); 17 18 Trace.Assert(list != null); 19 20 if (list.Count == 0) 21 return null; 22 else 23 return list[0]; 24 25 }
當我貼出這段代碼時,心情有些坎坷,因為我本人在這里也糾結了很久,這也是我一直沒有將斷言和異常划清界線的原因之一。
首先我們來回顧一下之前定義的斷言使用場景:內部不可能發生的錯誤。
selector.Return這段代碼是不是內部代碼?如果我們能夠修改Return中的代碼,說明它是內部代碼;反之,說明它是外部代碼。對於內部代碼,我們可以用斷言來保護其邏輯的不變性,當斷言被觸發時,我們就可以確信是內部代碼的錯誤,我們應該立即修復。
再糾結一下,假設Return是外部代碼,我們沒有辦法去修改它。那么上面的代碼可以有兩種寫法(如果你有更多的想法,請賜教)。
第一種,直接拋出異常。
If(list == null) { throw new NullReferenceException(); }
第二種,調整代碼。
if(list == null || list.Count == 0) { return null; } else { return list[0]; }
當然,還有一種就是什么也不做,讓代碼執行下去直至系統為你拋出空引用錯誤。但這種做法違背了防卸性編程的原則,我們總是應行盡早或離錯誤的發生地最近的地方處理錯誤,避免錯誤數據流向系統的其它地方,產生更加嚴重的錯誤。
總結
對異常或斷言的使用取決於你要防卸的是一個內部錯誤還是外部錯誤以及你認為它是一個內部錯誤或外部錯誤。如果你決定防卸一個內部錯誤,那請果斷使用斷言,反之,請使用異常。
參見:
.net 的異常繼承樹(http://msdn.microsoft.com/en-us/library/z4c5tckx(v=vs.110).aspx)
原代碼來至於我的TeamView開源項目(http://teamview.codeplex.com)
巜從小工到大師》