C# Notizen 10 異常處理

應(yīng)用程序執(zhí)行時,可能遇到各種可能的錯誤。C#使用異常來處理這些錯誤,異常將有關(guān)錯誤的信息封裝在一個類中。異常設(shè)計用于報告故障,它們提供了報告和響應(yīng)錯誤的一致方式。設(shè)計異常的初衷并非要提供一種程序流程控制機(jī)制、報告成功狀態(tài)或用作反饋機(jī)制,相反,它們報告程序執(zhí)行期間發(fā)生的故障。

在很多語言(尤其是非面向?qū)ο笳Z言)中,報告故障的標(biāo)準(zhǔn)機(jī)制是使用返回編碼(return code)。然而,在面向?qū)ο笳Z言中,并非總可以使用返回編碼,而取決于故障發(fā)生的背景。另外,返回編碼很容易被忽略;如果不想忽略它,就需要在有可能出現(xiàn)故障的地方做妥善處理,這非常復(fù)雜。

一、理解異常

異常能夠以清晰、簡潔、安全的方式表示運行期間發(fā)生的故障,它通常包含有關(guān)故障原因的詳細(xì)信息,其中包括調(diào)用棧跟蹤,它指出了當(dāng)前代碼塊正常返回時的執(zhí)行路徑。
異常并非要提供一種處理預(yù)期錯誤(如用戶操作或輸入可能導(dǎo)致的錯誤)的方式。對于這些錯誤,通過核實操作或輸入正確來防范錯誤要好得多。異常也并非要防范編碼錯誤。編碼錯誤可能導(dǎo)致異常,但對于這種錯誤,應(yīng)進(jìn)行修復(fù),而不是依賴于異常。

發(fā)生異常時,如果應(yīng)用程序顯式地提供了此時應(yīng)執(zhí)行的代碼,異常將得到處理;如果沒有這樣的代碼,就將出現(xiàn)未處理的異常。
所有異常都是從 System.Exception 派生而來的。當(dāng)托管代碼調(diào)用非托管代碼或外部服務(wù)(如 Microsoft SQL Server)并發(fā)生錯誤時,.NET 運行時將把錯誤條件包裝在一個從System.Exception派生而來的異常中。

ps:RuntimeWrappedException
在諸如 C++等語言中,可引發(fā)任何類型的異常,而不僅僅是從System.Exception 派生而來的異常。在這種情況下,通用語言運行時將把這些異常包裝在一個 RuntimeWrappedException 中。這確保了語言之間的兼容性。

System.Exception有多個屬性提供了有關(guān)錯誤的詳細(xì)信息:

  • 最常用的是Message屬性,它通過用戶能夠明白的描述詳細(xì)地指出了導(dǎo)致異常的原因。通常,這個屬性的內(nèi)容是幾個句子,對錯誤進(jìn)行了大致描述。
  • StackTrace屬性包含調(diào)用棧跟蹤,有助于判斷錯誤發(fā)生在什么地方。如果調(diào)試信息在運行期間可用,調(diào)用棧跟蹤將指出錯誤發(fā)生在哪一行及其所屬的源文件。
  • 當(dāng)一個異常被包裝在另一種異常內(nèi)時,通常使用屬性InnerException。原始異常存儲在屬性InnerException中,讓錯誤處理代碼能夠檢查原始信息。
  • HelpLink屬性可包含指向幫助文件的 URL,而幫助文件包含有關(guān)異常的更詳細(xì)信息。
  • Data屬性是一個 IDictionary(字典的非泛型版本)對象,用于以鍵/值對的方式存儲任意信息。

使用標(biāo)準(zhǔn)異常
雖然.NET Framework提供了 200多個公有異常類,但只有大約 15個是常用的,其他異常通常是從這些標(biāo)準(zhǔn)異常派生而來的。
除Exception外,其他兩個主要的基類是SystemException和ExternalException,其中前者是運行時生成的所有異常的基類,后者是在運行時的外部環(huán)境中發(fā)生或針對這類環(huán)境的異常的基類。
對于Exception、SystemException和ExternalException,應(yīng)只將它們用作更具體的派生異常的基類,還應(yīng)避免從SystemException直接派生出自定義異常。

其他標(biāo)準(zhǔn)異常如下表所示,它們分兩類:運行時引發(fā)的異常,您在代碼中不應(yīng)引發(fā)它們;可在代碼中(也應(yīng)該在代碼中)引發(fā)的異常。

異常類型 描述
IndexOutOfRangeException 僅當(dāng)使用錯誤的索引訪問數(shù)組或集合時,才有由Runtime引發(fā)
NullReferenceException 僅當(dāng)對null引用接觸引用時,才由Runtime引發(fā)
AccessViolationException 僅當(dāng)訪問霧效內(nèi)存時,才由Runtime引發(fā)
InvalidOperationException 在無效狀態(tài)下由成員引發(fā)
ArgumentException 所有參數(shù)異常的基類
ArgumentNullException 由不允許參數(shù)為null的方法引發(fā)
ArgumentOutOfRangeException 由驗證參數(shù)是否位于給定范圍內(nèi)的方法引發(fā)
COMException 封裝COM HRESULT信息的異常
SEHException 封裝Win32 結(jié)構(gòu)化異常處理信息的異常
OutOfMemoryException 在沒有足夠的內(nèi)存供程序繼續(xù)執(zhí)行時,由Runtime引發(fā)
StackOverflowException 因嵌套的方法調(diào)用過多(這通常是由于遞歸太深或無限遞歸導(dǎo)致的),導(dǎo)致執(zhí)行棧溢出時,由Runtime引發(fā)
ExecutionEngineException 在CLR的執(zhí)行引擎內(nèi)部錯誤時,由Runtime引發(fā)

驗證傳遞給您的公有方法的參數(shù)時,如果傳遞的參數(shù)不正確,就應(yīng)引發(fā)ArgumentException或其子類。
如果傳遞給方法的參數(shù)為null,就應(yīng)引發(fā)ArgumentNullException;如果傳遞的參數(shù)不在可接受的范圍內(nèi),就應(yīng)引發(fā)ArgumentOutOfRangeException。
如果就對象的當(dāng)前狀態(tài)而言,訪問屬性或調(diào)用方法不合適,就可引發(fā)InvalidOperationException。這不同于ArgumentException,是否引發(fā)ArgumentException不依賴于對象的狀態(tài)。

ps:驗證參數(shù)
如果調(diào)用方不好判斷參數(shù)是否有效,您應(yīng)考慮提供一種方法,讓它們能夠?qū)?shù)進(jìn)行檢查。
其他標(biāo)準(zhǔn)異常都是運行時專用的異常,您不應(yīng)在代碼中引發(fā)它們,也不應(yīng)從它們派生出自定義異常。

應(yīng)對參數(shù)進(jìn)行檢查,以防止發(fā)生IndexOutOfRangeException或NullReferenceException。

二、引發(fā)異常
要引發(fā)異常,可使用關(guān)鍵字throw。由于Exception是一個類,因此您必須使用關(guān)鍵字new創(chuàng)建其實例。如下代碼引發(fā)一個Exception異常:

throw new System.Exception();

異常引發(fā)后,程序?qū)⒘⒓赐V箞?zhí)行,而異常將沿調(diào)用棧向上傳遞,并尋找合適的處理程序。如果沒有找到處理程序,就將發(fā)生下述3種情況之一。

  • 如果異常發(fā)生在構(gòu)造函數(shù)內(nèi),就將終止執(zhí)行該構(gòu)造函數(shù),并調(diào)用基類的析構(gòu)函數(shù)(如果有)。
  • 如果調(diào)用棧中包含靜態(tài)構(gòu)造函數(shù)或靜態(tài)字段初始值設(shè)定項,就將引發(fā)TypeInitializationException,其InnerException屬性包含原始異常。
  • 如果到達(dá)了線程開頭,線程就將終止。在大多數(shù)情況下,這意味著如果異常到達(dá)Main( )方法后,仍未找到兼容的處理器,應(yīng)用程序就將終止。無論異常源自哪個線程,都將導(dǎo)致這樣的結(jié)果。

要確定什么情況下應(yīng)引發(fā)異常,需要明白編碼錯誤和執(zhí)行錯誤之間的差別。編碼錯誤可通過修改代碼來避免,因此沒有理由使用異常來處理這類錯誤。編碼錯誤可在編譯階段修復(fù),因此可采取措施確保它們不會在運行階段發(fā)生。
ps:System.Environment.FailFast
如果應(yīng)用程序處于不能安全地繼續(xù)執(zhí)行下去的境地,應(yīng)考慮調(diào)用System.Environment.FailFast,而不應(yīng)引發(fā)異常。
如果繼續(xù)執(zhí)行會導(dǎo)致安全風(fēng)險,如無法恢復(fù)的安全損害,那么也應(yīng)考慮調(diào)用FailFast。
遇到意外的錯誤條件時,將引發(fā)異常。這通常發(fā)生在類成員無法執(zhí)行其操作時。這種執(zhí)行錯誤不能完全避免,不管在代碼中采取多少防范措施。對于程序執(zhí)行錯誤,可通過代碼以編程方式進(jìn)行處理,但系統(tǒng)執(zhí)行錯誤無法通過代碼進(jìn)行處理。

三、處理異常
要處理異常,可使用異常對象和保護(hù)區(qū)域(protected regions)??蓪⒈Wo(hù)區(qū)域視為特殊的代碼塊,設(shè)計用于讓你能夠處理一樣。幾乎任何代碼行都可能導(dǎo)致異常,但大多數(shù)應(yīng)用程序?qū)嶋H上不需要處理這些異常。僅當(dāng)能夠采取有意義的措施時,才應(yīng)對異常進(jìn)行處理。
在C#中,可使用關(guān)鍵字try聲明保護(hù)區(qū)域(也叫try塊),并將要保護(hù)的語句用大括號括起,而相關(guān)的處理程序放在右大括號的后面。必須至少將下述處理程序之一與保護(hù)區(qū)域相關(guān)聯(lián)。

  • finally處理程序:它在退出保護(hù)區(qū)域時執(zhí)行,即使發(fā)生異常也是如此。保護(hù)區(qū)域最多可以有一個finally處理程序。
  • catch處理程序:它與特定異?;蚱渥宇惼ヅ?。保護(hù)區(qū)域可以有多個 catch處理程序,但特定類型的異常只能有一個catch處理程序。

發(fā)生異常時,將首先確定當(dāng)前指令(導(dǎo)致異常的指令)所屬的保護(hù)區(qū)域是否有與異常匹配的 catch 處理程序。如果當(dāng)前方法沒有匹配的處理程序,就將在調(diào)用方處查找,這個過程將不斷重復(fù)下去,直到找到匹配的處理程序或到達(dá)調(diào)用棧頂,到達(dá)調(diào)用棧頂后,應(yīng)用程序?qū)⒔K止。如果找到匹配的處理程序,就將返回到發(fā)生異常的地方,執(zhí)行finally處理程序,然后執(zhí)行catch處理程序。

ps:通用catch處理程序
可僅使用關(guān)鍵字catch來指定catch處理程序,這被稱為通用catch處理程序,但不應(yīng)這樣做。
在.NET Framework 1.0和 1.1中,存在這樣的情況,即非托管代碼可能引發(fā)運行時未能妥善處理的異常。
因此,這種異常沒有包裝到 System.Exception 派生異常中,除空 catch塊外的其他catch塊都無法捕獲它。
.NET 2.0修復(fù)了這種問題,現(xiàn)在這些異常被包裝到RuntimeWrappedException (它是從System.Exception派生而來的)中,因此不再需要空catch塊。
根據(jù)保護(hù)區(qū)域提供了哪些處理程序,可將其分為3類。

  • Try-Catch:只提供一個或多個 catch處理程序。
  • Try-Finally:只提供了一個 finally處理程序。
  • Try-Catch-Finally:提供了一個或多個 catch處理程序以及一個 finally處理程序。

編寫 catch 塊時,可只指定異常類型,也可同時指定異常類型和標(biāo)識符,這種標(biāo)識符稱為catch處理程序變量。catch處理程序變量讓您能夠在catch塊中引用異常對象,由于該變量的作用域為特定catch處理程序,且只會執(zhí)行一個catch處理程序,因此同一個標(biāo)識符可用于多個catch處理程序。然而,不能將該標(biāo)識符用于方法參數(shù)或其他局部變量。
如下栗子為聲明catch處理程序

try
{    
  int divisor = Convert.ToInt32(Console.ReadLine());    
  int result = 3 / divisor;
}
catch (DivideByZeroException ex)
{    
  Console.WriteLine(ex.Message);
}

如果有多個嵌套的try塊,且每個try塊都有匹配的catch處理程序,那么執(zhí)行哪個catch處理程序取決于嵌套順序。找到并執(zhí)行匹配的catch處理程序后,就不會執(zhí)行其他的catch處理程序。
如下示例捕獲多種異常

try
{    
  int divisor = Convert.ToInt32(Console.ReadLine());    
  int result = 3 / divisor;
}
catch (DivideByZeroException)
{    
  Console.WriteLine("Attempted to divide by zero");
}
catch (FormatException)
{    
  Console.WriteLine("Input was not in the correct format");
}
catch (Exception)
{    
  Console.WriteLine("General catch handler");
}

如上try塊導(dǎo)致DivideByZeroException,那么輸出將為Attempted to divide by zero;如果該 try塊導(dǎo)致FormatException,那么輸出將為 Input was not in the correct format;如果導(dǎo)致其他異常,那么輸出將為General catch handler。如果重新排列catch塊的順序,就將通用catch塊放在最前面,程序?qū)⒉荒芡ㄟ^編譯。

ps:隱藏異常
程序員經(jīng)常編寫這樣的catch塊,即除將錯誤寫入日志外什么也不錯。有時候這很重要,但通常是頂級調(diào)用方這樣做,并非每個方法都需要這樣做。捕獲異常后什么也不做,而旨在禁止它沿調(diào)用鏈向上傳遞時,將導(dǎo)致被稱為隱藏異常(swallowing exception)的問題。由于這只是隱藏了異常,而沒有對異常做任何處理,這可能導(dǎo)致難以跟蹤的間歇性問題。
最佳的做法是,僅當(dāng)在異常發(fā)生后需要做有意義的清理工作(如關(guān)閉文件或斷開數(shù)據(jù)庫連接)時,才捕獲異常。僅當(dāng)異常發(fā)生時才會執(zhí)行 catch 處理程序,因此不應(yīng)使用它來做清理工作。如果有清理工作要做,就應(yīng)在finally處理程序中進(jìn)行。這意味著除非要在發(fā)生異常時采取某種措施,否則應(yīng)使用try-finally,而不是try-catch或try-catch-finally。

ps:一般而言,不要在代碼中捕獲不具體的異常,如 Exception、SystemException 和ExternalException。另外,也不要捕獲關(guān)鍵的系統(tǒng)異常,如 StackOverflowException 和OutOfMemoryException。對于這些類型的異常,通常無法采取有意義的措施,而捕獲它們可能隱藏問題,導(dǎo)致調(diào)試和故障排除工作更復(fù)雜。

ps:惡化狀態(tài)異常
惡化狀態(tài)異常(corrupted state exception)指的是在應(yīng)用程序外(如OS內(nèi)核)發(fā)生的異常,這意味著運行進(jìn)程的完整性可能遭到破壞。有大約 12種惡化狀態(tài)異常,它們不同于常規(guī)異常。
常規(guī)異常和惡化狀態(tài)異常之間的差別不在于異常的類型,而在于引發(fā)異常的環(huán)境。默認(rèn)情況下,無法捕獲惡化狀態(tài)異常,哪怕捕獲Exception也不行。

四、重新引發(fā)捕獲的異常
如果捕獲異常只為將錯誤信息寫入日志或執(zhí)行沒有實際處理異常的操作,就應(yīng)重新引發(fā)異常。通過重新引發(fā)異常,可讓它繼續(xù)沿調(diào)用棧傳遞,以尋找合適的catch處理程序。
關(guān)鍵字throw除用于指定要引發(fā)的新異常外,還可用于重新引發(fā)異常。
為保留調(diào)用棧跟蹤信息,重新引發(fā)異常時應(yīng)只使用關(guān)鍵字throw,即使catch處理程序包含catch處理程序變量。
如下代碼演示了兩種重新引發(fā)異常的方法,它們之間存在細(xì)微的差別,這種差別在于隨異常傳遞的調(diào)用棧跟蹤信息。

try
{    
  // Some operation that results in an InvalidOperationException
}
catch (InvalidOperationException ex)
{    
  Console.WriteLine("Invalid operation occurred");    
  throw ex;
}
catch (Exception)
{    
  Console.WriteLine("General catch handler");    
  throw;
}

在第一種方法中,InvalidOperationException的 catch處理程序使用了 throw ex,這將以出現(xiàn)故障的方法為分界點截斷調(diào)用棧跟蹤信息。這意味著如果查看調(diào)用棧跟蹤信息,那么看起來異常源自代碼。但情況并非總是如此,尤其是在向上傳遞CLR生成的異常(如SqlException)時。

這種問題被稱為“破壞調(diào)用?!?,因為不再有完整的調(diào)用棧跟蹤信息。要核實這一點,可查看這兩個代碼塊的IL代碼。在IL代碼中,這一點很明顯,因為第一種方法的IL指令為throw,而第二種方法的IL指令為rethrow。

包裝異常
也可將捕獲的異常包裝在另一個異常中,然后引發(fā)新的異常。當(dāng)實際異常在上一層環(huán)境中沒有意義時,經(jīng)常這樣做。
包裝異常時,將在 catch 處理程序中引發(fā)新異常,并在其中包含原始異常。由于調(diào)用棧跟蹤信息將重置到新異常處,因此只有通過包含原始異常,才能獲悉原始調(diào)用棧跟蹤信息和異常細(xì)節(jié)。如下代碼演示了引發(fā)包裝的異常的正確方式。

try
{    
  // Some operation that can fail
}
catch (Exception ex)
{    
  throw new InvalidOperationException("The operation failed", ex);
}

應(yīng)慎用異常包裝,并做到深思熟慮。只要對是否應(yīng)包裝異常存在任何疑問,就不要包裝它。包裝異??赡軐?dǎo)致無法獲悉發(fā)生錯誤的方法和位置,進(jìn)而需要花大量時間調(diào)試問題;還可能導(dǎo)致調(diào)用方難以處理異常,因為它們不僅需要處理當(dāng)前異常,還需要處理包裝的異常—提取并處理真正的異常。

五、溢出和整型算術(shù)運算
所有基本數(shù)值數(shù)據(jù)類型的取值范圍都是固定的。要獲悉該范圍的上限和下限,可分別使用屬性MaxValue和MinValue。如果試圖給int.MaxValue加1,那么結(jié)果將如何呢?
這個問題的答案取決于編譯器設(shè)置。默認(rèn)情況下,C#編譯器允許發(fā)生溢出,而上述運算的結(jié)果為可能的最大負(fù)值。在很多情況下,這種行為都是可以接受的,因為其風(fēng)險很低。
如果對于每次整型算術(shù)運算都執(zhí)行溢出檢查,其開銷將導(dǎo)致性能急劇下降,與此相比,溢出帶來的風(fēng)險微不足道。
如果要避免整型算術(shù)運算的溢出風(fēng)險,即控制溢出檢查行為,那么可以使用關(guān)鍵字checked和unchecked。這些關(guān)鍵字能夠顯式地控制溢出檢查,因為它們將覆蓋編譯器設(shè)置。

ps:檢查和不檢查
在檢查環(huán)境中,導(dǎo)致算術(shù)運算溢出的語句將引發(fā)異常;在不檢查環(huán)境中,溢出將被忽略,而結(jié)果將被截斷。只有一些數(shù)值運算受這種設(shè)置的影響。
(1)整型類型之間的顯式轉(zhuǎn)換。
(2)使用如下運算符的表達(dá)式:++、??、單目?、+、?、*、/。
關(guān)鍵字 checked 和 unchecked 可用于語句塊,這將影響語句塊中所有的整型算術(shù)運算,如下所示

int max = int.MaxValue;
checked 
{    
  int overflow = max++;    
  Console.WriteLine("The integer arithmetic resulted in an OverflowException.");
}
unchecked
{    
  int overflow = max++;    
  Console.WriteLine("The integer arithmetic resulted in a silent overflow.");
}

這些關(guān)鍵字還可用于整型運算表達(dá)式,這樣表達(dá)式將在檢查或不檢查環(huán)境下計算。在這種情況下,必須用括號將表達(dá)式括起。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文部分來自于:代碼鋼琴家blog address:www.cnblogs.com/lulipro/p/75042...
    八目朱勇銘閱讀 1,403評論 0 4
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,688評論 18 399
  • Java異??刂茩C(jī)制又被稱為“違例控制機(jī)制”。捕獲程序錯誤最理想的時機(jī)是在編譯階段,這樣可以徹底避免錯誤的代碼運行...
    kelgon閱讀 4,602評論 2 50
  • 聽說太陽來了 就躲進(jìn)幕后 黑色的夜 成了隔離的壕溝 金雞鳴啼召喚 變成最后的挽留 一絲光從東方露出 所有的打點都交...
    一池凹水凸龍閱讀 215評論 2 3
  • 天底下所有的勞動都是辛苦的,天底下所有勞動的成果都是甜蜜的,正所謂苦是甜的根,甜是苦的果。而把辛苦推到甜蜜極致的,...
    湖邊人老劉閱讀 388評論 0 2

友情鏈接更多精彩內(nèi)容