淺析CLR的異常處理模型

文章目錄:

  1. 異常概述
  2. CLR中的異常處理機制
  3. CLR中異常的核心類System.Exception類
  4. 異常處理的設計規範和最佳實踐
  5. 異常處理的性能問題
  6. 其餘拓展

一、異常概述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         }
View Code

  在catch塊的結尾,咱們有三個選擇:ui

    • 從新拋出相同異常
    • 拋出一個不一樣的異常  
    • 讓線程從catch塊底部退出(把異常吞掉)  

  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         }
View Code

  對於系統拋出異常,能夠向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         }
FirstChanceException

  當方法沒法完成指明的任務的時候,就應該拋出一個異常。拋出異常時應該注意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         }
Exception以及子類

 四、異常處理的設計規範和最佳實踐

  1. 善用finally塊,在執行catch塊和finally塊中的代碼的時候,CLR不容許線程終止。因此,finally塊中代碼始終會執行,應該先用finally塊清理那些已經成功啓動的操做,再返回至調用者或者執行finally塊以後的代碼;利用finally塊中代碼顯示釋放對象避免資源泄露。
    • 例如使用lock語句,鎖將在finally塊中被釋放。
    • 使用using語句時候,finally塊中調用對象的Dispose方法。
    • foreach語句,再finally方法中調用IEnumerator對象的Dispose方法。
    • 析構方法,在finally塊中調用基類的Finalize方法。
  2. 不要什麼都捕捉,不要過於頻繁的,不恰當的使用catch塊。不要把異常吞噬掉,而是應該容許一場在調用棧中向上移動,讓應用程序代碼針對性處理。
  3. 得體的從異常中恢復。
  4. 發生不可恢復的異常時,回滾部分完成的操做來維持狀態。
    • 例如要序列化一組對象到磁盤文件,當中途失敗時,要文件回滾到對象序列化以前的狀態。
  5. 隱藏實現細節來維繫協定;例如如今有一個獲取用戶電話號碼的功能,經過輸入名字,從文件中找到匹配號碼並返回。假如文件不存在或者文件讀取異常,這時候就不該該將這兩個異常信息返回給用戶,應該返回一個自定義的用戶還沒有找到該用戶的號碼這樣的異常給調用者。 如下是僞代碼:
     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     }
    View Code
  6.  對於未處理的異常會形成進程終止,這些異常能夠在windows日誌中查看。具體位置爲事件管理器->windows日誌->應用程序。

五、異常處理的性能問題

  對於非託管代碼,例如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修飾的方法。

相關文章
相關標籤/搜索