一、異常概述sql
異常咱們一般指的是行動成員(例如類實例對象)沒有完成所宣稱的行動或任務。
數據庫
例以下圖中代碼,返回 "Lmc"這個字符串的第二個字符的大寫是否爲 "M",假如這個執行過程當中任何一個步驟出錯,都應該返回一個狀態(例如"L".Substring(1,1)會由於字符串索引不夠長而出現異常),指示代碼不能正常進行完成行動,可是如下這句代碼是沒有辦法返回的,因此.net framework 使用異常處理來解決這個問題,拋出特定異常("L".Substring(1,1)會拋出ArgumentOutOfRangeException異常)。windows
二、CLR中的異常處理機制app
C#中的異常處理機制是使用try , catch ,finally關鍵字來包裹代碼,捕獲異常,以及執行恢復清理操做。使用規範是try塊中寫入正常執行/須要清理的代碼,catch塊捕獲特定異常,執行回覆操做,finally塊執行清理代碼。dom
其中catch塊會優先捕捉特定的異常。例如try塊拋出異常,CLR會搜索與try塊同級的,捕捉類型與throw類型相同的的catch塊,假如沒有找到,CLR會調用棧更高的一層去搜索與異常類型相匹配的catch塊。假如到了調用棧頂部,依舊沒有找到匹配的catch塊,就會發生無處裏的異常。ide
當CLR找到匹配的catch塊,就會執行內層全部finally塊代碼,而後執行catch塊,執行與捕獲catch塊相同級的finally代碼。 以下如所示:性能
1 private static void Exfun1() 2 { 3 try 4 { 5 Exfun2(); 6 } 7 catch(Exception ex) 8 { 9 Console.WriteLine($" this is Exfun1 Exception : {ex.StackTrace}"); //3 10 } 11 finally 12 { 13 Console.WriteLine("this is Exfun1 finally"); //4 14 } 15 } 16 private static void Exfun2() 17 { 18 try 19 { 20 Console.WriteLine("this is Exfun2"); //1 21 throw new IOException(); 22 } 23 catch(InvalidCastException ex) 24 { 25 Console.WriteLine($"this is Exfun2 InvalidCastException {ex.Message}"); //因爲捕獲的異常與拋出的異常不匹配,因此不執行 26 } 27 finally 28 { 29 Console.WriteLine("this is Exfun2 finally"); //2 因爲是在Exfun1中的catch捕獲到異常,因此先執行內層的catch塊。 30 } 31 }
在catch塊的結尾,咱們有三個選擇:ui
finally塊執行與try塊中行動須要的資源清理操做。(例如try塊中打開了一個數據庫鏈接,finally塊中執行sqlconnection.close();sqlconnection.dispose();)this
catch塊和finally塊中的代碼應該很是短,並且具備很高的執行成功率,避免catch塊和finally塊中代碼再次拋出異常。當出現異常直至調用棧頂部都沒有正確的catch捕獲,就會產生一個未處理的異常,這時CLR會終止執行的進程,保護數據被進一步損壞。spa
三、CLR中異常的核心類System.Exception類
CLR中容許異常拋出任意類型,例如int string,可是根據CLS(公共語言規範),C#只能拋出派生自System.Exception的類。
當一個異常拋出被catch塊捕捉時,CLR會記錄catch捕獲的位置,CLR會建立一個字符串賦值給Exception類的StackTrace屬性。catch塊中從新拋出捕獲的異常會致使CLR重置異常起點。例如:
1 private static void SomeMehtod() 2 { 3 try 4 { 5 Console.WriteLine("this is someMthod1"); 6 SomeMethod2(); 7 } 8 catch (Exception e) 9 { 10 Console.WriteLine($"method1 reset exception line {e.StackTrace}"); 11 } 12 } 13 private static void SomeMethod2() 14 { 15 try 16 { 17 Console.WriteLine("this is someMthod2"); 18 throw new IOException(); 19 } 20 catch (IOException e) 21 { 22 Console.WriteLine($"method2 exception line {e.StackTrace}"); 23 throw e; 24 } 25 }
假如想較準確知道錯誤位置,可使用以下寫法:
1 private void SomeMethodNoReset() 2 { 3 bool trySucceeds = false; 4 try 5 { 6 //dosomething 7 trySucceeds = true; 8 } 9 finally 10 { 11 if (!trySucceeds) 12 { 13 14 } 15 } 16 }
對於系統拋出異常,能夠向AppDomain的FirstChanceException事件登記,這樣,只要在這個Appdomain(應用程序域)中發生異常,就能夠獲得通知:
1 static void Main(string[] args) 2 { 3 var thisdomain = Thread.GetDomain(); 4 thisdomain.FirstChanceException += Thisdomain_FirstChanceException; 5 Exfun1(); 6 .... 7 } 8 private static void Thisdomain_FirstChanceException(object sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e) 9 { 10 Console.WriteLine($"appdomain 中FirstChanceException事件登記發生的異常{e.Exception.Message}"); 11 }
當方法沒法完成指明的任務的時候,就應該拋出一個異常。拋出異常時應該注意2點:一、拋出的異常應該是一個有意義的類型,建議使用寬而淺的異常類,儘可能少的使用基類。二、向異常類傳遞的信息應該指明爲什麼沒法完成任務,幫助開發人員修正代碼。
如下是使用反射加載的Exception的類以及子類的部分截圖
1 private static void Go() 2 { 3 LoadAssemblies(); 4 var allTypes = (from a in AppDomain.CurrentDomain.GetAssemblies() 5 from t in a.ExportedTypes 6 where typeof(Exception).GetTypeInfo().IsAssignableFrom(t.GetTypeInfo()) 7 orderby t.Name 8 select t).ToArray(); 9 Console.WriteLine(WalkInherirtanceHierarchy(new StringBuilder(), 0, typeof(Exception), allTypes)); 10 } 11 private static StringBuilder WalkInherirtanceHierarchy(StringBuilder sb, int indent, Type baseType, IEnumerable<Type> allTypes) 12 { 13 string spaces = new string(' ', indent * 3); 14 sb.AppendLine(spaces + baseType.FullName); 15 foreach (var t in allTypes) 16 { 17 if (t.GetTypeInfo().BaseType != baseType) continue; 18 WalkInherirtanceHierarchy(sb, indent + 1, t, allTypes); 19 } 20 return sb; 21 } 22 private static void LoadAssemblies() 23 { 24 String[] assemblies = { 25 "System, PublicKeyToken={0}", 26 "System.Core, PublicKeyToken={0}", 27 "System.Data, PublicKeyToken={0}", 28 "System.Design, PublicKeyToken={1}", 29 "System.DirectoryServices, PublicKeyToken={1}", 30 "System.Drawing, PublicKeyToken={1}", 31 "System.Drawing.Design, PublicKeyToken={1}", 32 "System.Management, PublicKeyToken={1}", 33 "System.Messaging, PublicKeyToken={1}", 34 "System.Runtime.Remoting, PublicKeyToken={0}", 35 "System.Runtime.Serialization, PublicKeyToken={0}", 36 "System.Security, PublicKeyToken={1}", 37 "System.ServiceModel, PublicKeyToken={0}", 38 "System.ServiceProcess, PublicKeyToken={1}", 39 "System.Web, PublicKeyToken={1}", 40 "System.Web.RegularExpressions, PublicKeyToken={1}", 41 "System.Web.Services, PublicKeyToken={1}", 42 "System.Xml, PublicKeyToken={0}", 43 "System.Xml.Linq, PublicKeyToken={0}", 44 "Microsoft.CSharp, PublicKeyToken={1}", 45 }; 46 47 const String EcmaPublicKeyToken = "b77a5c561934e089"; 48 const String MSPublicKeyToken = "b03f5f7f11d50a3a"; 49 50 Version version = typeof(System.Object).Assembly.GetName().Version; 51 52 foreach (String a in assemblies) 53 { 54 String AssemblyIdentity = 55 String.Format(a, EcmaPublicKeyToken, MSPublicKeyToken) + 56 ", Culture=neutral, Version=" + version; 57 Assembly.Load(AssemblyIdentity); 58 } 59 }
四、異常處理的設計規範和最佳實踐
1 public sealed class PhoneBook 2 { 3 private string m_pathname; //地址簿文件路徑名稱 4 public string GetPhoneNumber(string name) 5 { 6 string phone; 7 FileStream fileStream = null; 8 try 9 { 10 //根據name從fs中讀取內容 11 fileStream = new FileStream(m_pathname, FileMode.Open); 12 byte[] bt = new byte[1000]; 13 fileStream.Read(bt, 0, 123); 14 phone = System.Text.Encoding.Default.GetString(bt); 15 return phone; 16 } 17 catch(FileNotFoundException ex) 18 { 19 //從新拋出一個不一樣的異常,並且加入name 20 //將原來的異常設置爲內部異常 21 throw new NameNotFoundException(name, ex); 22 } 23 catch(IOException ex) 24 { 25 throw new NameNotFoundException(name, ex); 26 } 27 } 28 } 29 public class NameNotFoundException : Exception { 30 public NameNotFoundException(string name,Exception e) { } 31 }
五、異常處理的性能問題
對於非託管代碼,例如C++,編譯器必須生成代碼來跟蹤有哪些對象被成功構造。編譯器還要生成代碼在異常被捕捉時候來調用已成功構造的對象的析構器。這會在應用程序生成大量的簿記代碼,影響代碼的大小和執行時間;
對於託管代碼,例如C#,由於託管對象是在託管堆中分配內存,因此這些對象受到GC的監控。若是對象被成功構造且拋出異常,將會由GC來釋放對象內存。編譯器不用生成簿記代碼來跟蹤成功構造對象,也不用由編譯器保證對象析構器的調用。
在遇到頻繁調用且頻繁失敗的方法,這時候拋出異常會形成巨大的性能損失。這時候在方法中可使用FCL提供的TryXxx方法。例如 int 的 TryParse。
六、其餘拓展(CER)
CER(約束執行區域)是必須對錯誤有適應力的代碼塊。在CLR的代碼執行過程當中,可能因爲AppDomain中的一個線程遇到未處理的異常從而致使進程中的整個AppDomain遭到卸載。AppDomain卸載時它的全部狀態都會卸載。因此CER通常用於處理多個AppDomain或進程共享的狀態。例如,當調用一個類型的靜態構造器時,可能拋出異常。這時候,假如是在catch塊或者finally塊中,錯誤恢復代碼和資源清理代碼就不能完整的執行。以下圖所示:由於調用Type1的M方法時候,會隱式調用M的靜態構造器,這樣finally中的代碼就不能完整的執行。
解決方案是使用CER,CER使用方法是在try塊代碼前添加 RuntimeHelpers.PrepareConstrainedRegions(); 在finlly塊執行的方法用ReliabilityContract特性修飾。這樣,JIT編譯器會提早編譯與try塊關聯的catch塊和finlly塊的代碼。而且會加載相應程序集,調用靜態構造器。JIT編譯器還會遍歷調用圖,提早準備用ReliabilityContract修飾的方法。