2015 年 11 月

第 30 卷,第 12 期

本文章是由機器翻譯。

Essential .NET - C# 例外狀況處理

Mark Michaelis | 2015 年 11 月

Mark Michaelis歡迎使用我的基本的.NET 資料行。就在這裡其中您將能夠遵循所有發生在 Microsoft.NET Framework 世界中是否有在 C# vNext (目前 C# 7.0)、 改良的.NET 內部或餘 Roslyn 和.NET 核心前面 (例如 MSBuild 轉向開放原始碼) 往前推進。

我已撰寫並使用.NET 進行開發因為他又宣 2000年中的預覽。我會撰寫有關的大部分不只是新東西,但是有關如何運用方針的最佳作法與技術。

我住 Spokane 現,我呼叫 IntelliTect 高階顧問公司"暨迷"(IntelliTect.com)。IntelliTect 具有卓越的專長是開發 「 硬東西 」。我已經在 20 年和 Microsoft 的區域經理的八個這些年來 Microsoft MVP (目前為 C# 中)。現在查看更新的例外狀況處理指導方針會啟動這個資料行。

C# 6.0 包含兩個新的例外狀況處理功能。首先,它包含例外狀況的支援 — 能夠提供篩選出例外狀況之前堆疊回溯時進入 catch 區塊的運算式。第二,它包含在 catch 區塊,非同步處理加入到語言時不可能在 C# 5.0 中的項目內的非同步支援。此外,有許多其他的 C# 和對應的.NET Framework 中,變更,也在某些情況下,就大到需要編輯 C# 程式碼撰寫指引的最後五個版本中所發生的變更。在這一期,我將檢閱這些變更的數目並提供更新的程式碼撰寫指引與相關例外狀況處理 — 攔截例外狀況。

攔截例外狀況: 檢閱

很不錯了解,擲回特定例外狀況類型可讓用來識別問題的例外狀況的型別本身籃子。它不需要,換句話說,攔截例外狀況和例外狀況訊息上使用 switch 陳述式來判斷要根據例外狀況所採取的動作。而 C# 允許多個 catch 區塊,每個目標的特定例外狀況型別中所示 圖 1

圖 1 攔截不同的例外狀況類型

using System;
public sealed class Program
{
  public static void Main(string[] args)
    try
    {
       // ...
      throw new InvalidOperationException(
         "Arbitrary exception");
       // ...
   }
   catch(System.Web.HttpException exception)
     when(exception.GetHttpCode() == 400)
   {
     // Handle System.Web.HttpException where
     // exception.GetHttpCode() is 400.
   }
   catch (InvalidOperationException exception)
   {
     bool exceptionHandled=false;
     // Handle InvalidOperationException
     // ...
     if(!exceptionHandled)
       // In C# 6.0, replace this with an exception condition
     {
        throw;
     }
    }  
   finally
   {
     // Handle any cleanup code here as it runs
     // regardless of whether there is an exception
   }
 }
}

當例外狀況發生時,執行會跳到可以處理第一個 catch 區塊。如果有多個相關聯 try 與一個 catch 區塊,比對的接近程度取決於 (假設沒有 C# 6.0 例外狀況) 的繼承鏈結並以符合第一個會處理例外狀況。例如,即使擲回例外狀況是輸入 System.Exception,這"是"關聯性是透過繼承因為 System.InvalidOperationException 最終都是衍生自 System.Exception。InvalidOperationException 最符合擲回的例外狀況,因為 catch(InvalidOperationException...) 將在攔截例外狀況並不會封鎖 catch(Exception...) 如果有的話。

Catch 區塊必須出現在 (一次假設沒有 C# 6.0 例外狀況) 的順序從最特定至最廣泛、 以避免發生編譯時期錯誤。例如,加入任何其他例外狀況之前 catch(Exception...) 區塊會導致編譯錯誤因為在其繼承鏈結中的某個時間點之前的所有例外狀況衍生自 System.Exception。也請注意 catch 區塊的具名的參數不是必要的。事實上,沒有參數類型的最後一個 catch 允許,不幸的是,底下的一般 catch 區塊所述。

有時候之後攔截例外狀況,您可能會決定事實上,就不可能適當地處理例外狀況。在此案例中,您有兩個主要選項。第一個選項是不同的例外狀況重新擲回。有三個當這是合理的常見案例:

案例 No.1 擷取例外狀況並未充分識別觸發它的問題。例如,在呼叫 System.Net.WebClient.DownloadString 具有有效的 URL 時,執行階段可能會擲回 System.Net.WebException 沒有網路連線時 — 相同具有不存在的 URL 會擲回的例外狀況。

案例 [否]。 2 擷取例外狀況包含不應該公開更高版本呼叫鏈結的私用資料。比方說,非常早期版本的 CLR v1 預先 alpha (甚至) 有說它,例外狀況 」 安全性例外狀況: 您沒有權限以決定 c:\temp\foo.txt 路徑。 」

案例 No.3 例外狀況型別是太特定呼叫者處理。例如,System.IO 時發生例外狀況 (例如 UnauthorizedAccessException IOException FileNotFoundException DirectoryNotFoundException PathTooLongException、 NotSupportedException 或 SecurityException ArgumentException) 伺服器上叫用 Web 服務來查閱郵遞區號。

當重新擲回不同的例外狀況,請注意它可能會失去原始的例外狀況的事實 (想必刻意在案例 2)。若要防止這個情況,設定包裝例外狀況的 InnerException 屬性,通常可以指派透過建構函式與攔截的例外狀況除非這麼做因此會公開不應該公開在呼叫鏈結中的更高版本的私用資料。如此一來,原始的堆疊追蹤是仍然可用。

如果您不要將內部例外狀況並仍然尚未指定例外狀況執行個體之後 throw 陳述式 (擲回例外狀況) 的位置堆疊追蹤將例外狀況執行個體上。即使您重新擲回例外狀況之前攔截到已經設定堆疊追蹤,它會重設。

攔截例外狀況是判斷事實上,您無法適當地處理它的第二個選項。在這種狀況下您會想要完全相同的例外狀況重新擲回 — 將它傳送至呼叫鏈結的下一個處理常式。InvalidOperationException catch 區塊的 圖 1 示範這項功能。Throw 陳述式顯示不會擲回的例外狀況的任何識別 (擲回為本身),即使出現在 catch 區塊範圍無法重新擲回的例外狀況執行個體 (例外狀況)。擲回特定例外狀況會更新以符合新的 throw 位置的所有堆疊資訊。如此一來,表示原本發生例外狀況的呼叫站台的所有堆疊資訊可能都會遺失,而大幅更難以診斷問題。在判斷 catch 區塊不能完全處理的例外狀況、 例外狀況應該重新擲回使用空白 throw 陳述式。

不論您是重新擲回相同的例外狀況或例外狀況的包裝,一般而言會避免例外狀況報告或記錄呼叫堆疊中較低。也就是說,不要記錄例外狀況每次您攔截並重新擲回它。這樣會導致不必要的雜亂的記錄檔而不會因為相同的動作將會記錄每次增加值。此外,這個例外狀況包含堆疊追蹤資料時它擲回的因此不需要記錄的每一次。所有可能是例外狀況時則會處理或記錄,它不會處理、 記錄來記錄之前關閉處理序的例外狀況的情況下。

擲回現有的例外狀況而不會取代堆疊資訊

在 C# 5.0 機制已將加入可擲回先前已擲回的例外狀況而不會遺失在原始的例外狀況的堆疊追蹤資訊。這可讓您重新的範例中,例外狀況擲回甚至從 catch 區塊之外並因此而不使用空白擲回。雖然很少需要執行此動作,但是在某些情況下例外狀況包裝或在 catch 區塊外部移動程式執行之前會儲存。例如,多執行緒程式碼可能會換行 AggregateException 的例外狀況。.NET Framework 4.5 提供 System.Runtime.ExceptionServices.ExceptionDispatchInfo 類別特別地處理這種情況下使用其靜態擷取和執行個體方法會擲回。圖 2 示範重新擲回例外狀況已不需要重設的堆疊追蹤資訊或使用空的 throw 陳述式。

圖 2 使用 ExceptionDispatchInfo 重新擲回例外狀況

using System
using System.Runtime.ExceptionServices;
using System.Threading.Tasks;
Task task = WriteWebRequestSizeAsync(url);
try
{
  while (!task.Wait(100))
{
    Console.Write(".");
  }
}
catch(AggregateException exception)
{
  exception = exception.Flatten();
  ExceptionDispatchInfo.Capture(
    exception.InnerException).Throw();
}

使用 ExeptionDispatchInfo.Throw 方法中,編譯器不會將它視為 return 陳述式也可能正常 throw 陳述式的方式相同。例如,如果方法簽章的傳回值,但從 ExceptionDispatchInfo.Throw 的程式碼路徑傳回任何值,編譯器會發出錯誤訊息指出已傳回任何值。有時候開發人員可能會強制進行 ExceptionDispatchInfo.Throw return 陳述式與即使這類陳述式永遠不會在執行階段執行 — 會改為擲回例外狀況。

在 C# 6.0 中攔截例外狀況

一般例外狀況處理的指導方針是為了避免攔截您將無法完整處理的例外狀況。不過,因為在 C# 6.0 之前的 catch 運算式可能只由以下篩選例外狀況型別能夠檢查例外狀況資料及回溯堆疊在 catch 區塊之前的內容所需 catch 封鎖成為之前檢查它的例外狀況的處理常式。不幸的是,在決定不是用來處理例外狀況時,將會很難撰寫可讓不同的 catch 區塊內相同的內容來處理例外狀況的程式碼。和,重新擲回相同的例外狀況結果必須以叫用兩段式例外狀況處理序同樣地,牽涉到先處理程序提供例外狀況向上呼叫鏈結直到發現一個會處理它第二,回溯呼叫堆疊的例外狀況和 catch 位置之間的每個框架。

一旦擲回例外狀況,而非回溯的呼叫堆疊的只是為了讓因為例外狀況的進一步檢查揭露無法充分處理它重新擲回的例外狀況的 catch 區塊,顯然會是不想在第一時間攔截的例外狀況。從 C# 6.0,額外的條件運算式是可供 catch 區塊。而不是限制只根據發生例外狀況型別相符的比對的捕捉區塊是否 C# 6.0 會包含支援條件式子句。當子句可讓您提供進一步篩選的布林運算式的捕捉區塊至唯一的控制代碼的例外狀況如果條件為 true。中的區塊 System.Web.HttpException 圖 1 此示範以等號比較運算子。

有趣的例外狀況結果是,提供編譯器不會強制 catch 區塊來繼承鏈結順序所出現的例外狀況時。比方說,型別為 System.ArgumentException 隨附的例外狀況的 catch 現在出現之前更特定的 System.ArgumentNullException 類型,即使後者是衍生自前者。這是重要的因為它可讓您撰寫更特定的例外狀況類型 (不論有無例外狀況) 後面的一般例外狀況型別成對的特定的例外狀況。執行階段行為會保持一致和舊版的 C# 中。第一個符合的 catch 區塊所捕捉到例外狀況。增加的複雜性只是在 catch 區塊是否符合取決於型別和例外狀況的組合和編譯器只會強制執行相對於沒有例外狀況的 catch 區塊的順序。例如,catch(System.Exception) 與例外狀況可以出現之前 catch(System.ArgumentException) 包含或不含例外狀況。不過,一旦不輸入例外狀況的 catch 例外狀況就會出現,當然沒有的更特定的例外狀況區塊 (假設 catch(System.ArgumentNullException)) 可能會發生的例外狀況有。這會交由程式設計師與 「 彈性 」 可能的順序不對的程式碼例外狀況 — 與較早的例外狀況攔截例外狀況適用於更新的甚至也可能呈現較新的不小心無法連上。最後,catch 區塊的順序是會排序 if else 陳述式的方式類似。一旦在條件符合時,會忽略所有其他的 catch 區塊。不過,與 if else 陳述式中的條件,不同的是所有的 catch 區塊必須包括例外狀況型別檢查。

更新的例外狀況處理指導方針

中的比較運算子範例 圖 1 輕而易舉的事,但不限於簡化例外狀況。您可以,例如進行方法呼叫來驗證條件。唯一的需求是運算式的述詞 — 它會傳回布林值。換句話說,您基本上可以執行任何您想從 catch 例外狀況的呼叫鏈結中的程式碼。這樣會增加可能永遠不會再次攔截並重新擲回相同的例外狀況一次。基本上,您就可以將範圍縮小內容夠之前攔截並只攔截它時這麼做是有效的例外狀況。因此,若要避免攔截您將無法完整處理的例外狀況的指導方針會變成成真。事實上,任何周圍的空白 throw 陳述式的條件式檢查可以可能與程式碼的氣味加上旗標和避免。請考慮加入而不必使用空的 throw 陳述式除了要保存處理程序終止之前的變動性狀態的例外狀況。

話雖如此,開發人員應該限制檢查僅限內容的條件式子句。因為本身的條件運算式會擲回例外狀況,然後略過該新的例外狀況和條件會被視為 false,這很重要。基於這個理由,您應該避免擲回例外狀況的條件運算式中的例外狀況。

一般 Catch 區塊

C# 需要程式碼擲回的任何物件必須衍生自 System.Exception。不過,這項需求不是用於所有語言通用的。例如,C/c + + 可讓擲回,包括不衍生自 System.Exception 或甚至基本的型別如 int 或 string 的 managed 例外狀況的任何物件型別。開始使用 C# 2.0,所有的例外狀況是否衍生自 System.Exception,將會傳播到 C# 組件為衍生自 System.Exception。結果是 System.Exception 的 catch 區塊會攔截所有"合理地處理 「 較早的區塊不會攔截的例外狀況。在 C# 1.0 之前不過,如果從方法呼叫擲回非 System.Exception 衍生的例外狀況 (位於組件不是以 C# 撰寫),就不會攔截例外狀況 catch(System.Exception) 區塊。基於這個理由,C# 也支援現在的行為相同 (System.Exception 例外狀況) 的 catch 區塊但沒有型別或變數名稱的一般 catch 區塊 (catch {})。這類區塊的缺點是動作的只是動作的存取任何例外狀況執行個體和因此無從得知合適。它甚至就不會記錄該例外狀況或識別不太可能其中這類例外狀況是無害的案例。

實際上,catch(System.Exception) 區塊和一般 catch 區塊 — 此處一般稱為如 catch System.Exception 區塊 — 都避免使用除了底下的 pretense 「 處理 」 例外狀況記錄之前關閉處理序。之後您可以處理的唯一攔截例外狀況的一般原則,似乎頗具說服力自負程式設計師宣告為其撰寫程式碼 — 這個 catch 可以處理可能會擲回的所有例外狀況。首先,努力目錄任何和所有例外狀況 (尤其是在其中執行程式碼的數量是最大的主要和可能的內容的主體最少) 似乎修訂版除了最簡單的程式。其次是一堆可能意外地擲回的例外狀況。

C# 4.0 時發生了第三組之前損毀的狀態例外狀況未甚至通常可復原程式。這個集合是在 C# 4.0 中,不過,開始擔心因為攔截 System.Exception (或一般 catch 區塊) 不會事實上抓取這類例外狀況。(技術上您可以來裝飾方法與 System.Runtime.ExceptionServices.HandleProcessCorruptedStateExceptions 如此就會攔截這些例外狀況,不過您可以完全解決這類例外狀況的可能性是極具挑戰性。請參閱 bit.ly/1FgeCU6 如需詳細資訊。)

請注意在損毀的狀態例外狀況的一個 technicality 是它們將只傳遞超過攔截 System.Exception 區塊時由執行階段擲回。事實上會攔截例如 System.StackOverflowException 或其他 System.SystemException 損毀的狀態例外狀況的明確擲回。不過,這類擲回會是非常容易讓人誤解和其實是基於回溯相容性才支援。現今的指導方針並不是擲回任何損毀的狀態例外狀況 (包括 System.StackOverflowException、 System.SystemException、 System.OutOfMemoryException、 System.Runtime.InteropServices.COMException、 System.Runtime.InteropServices.SEHException 和 System.ExecutionEngineException)。

在 [摘要] 請避免使用 System.Exception 封鎖除非它是處理某些清除程式碼與例外狀況的 catch 和記錄的例外狀況重新擲回或正常關閉應用程式之前。例如,如果 catch 區塊中無法成功儲存 (項目無法一定是假設為,也可能已損毀) 的任何變動性資料之前先關閉應用程式或重新擲回例外狀況。當遇到的應用程式應該終止,因為它會繼續執行不安全的案例、 程式碼應該叫用 System.Environment.FailFast 方法。避免 System.Exception 和除了依正常程序記錄例外狀況之前先關閉應用程式的一般 catch 區塊。

總結

我可以在這篇文章提供更新的指導方針來例外狀況處理 — 捕捉例外狀況、 C# 和.NET Framework 中發生在最後幾個版本的增強功能所造成的更新。儘管有一些新的指導方針,許多都仍然只是為公司做為之前。以下是指導方針攔截例外狀況的摘要:

  • 避免攔截您將無法完整處理的例外狀況。
  • 避免隱藏 (捨棄) 完全未處理的例外狀況。
  • 請勿使用 throw 來重新擲回例外狀況。而不是在 catch 區塊內會擲回 < 例外狀況物件 >。
  • 除非執行的話會公開私用資料來進行設定攔截的例外狀況包裝例外狀況的 InnerException 屬性。
  • 請考慮改為不必重新擲回例外狀況之後擷取一個無法處理的例外狀況。
  • 請避免擲回例外狀況的例外狀況的條件運算式。
  • 時小心重新擲回不同的例外狀況。
  • 很少使用 System.Exception 和一般 catch 區塊 — 但若要記錄例外狀況之前先關閉應用程式。
  • 避免例外狀況報告或記錄在呼叫堆疊中較低。

移至 itl.tc/ExceptionGuidelinesForCSharp 檢閱每一個元素的詳細資料。在未來的專欄中我打算更加專注於擲回例外狀況的方針。可以滿足說的現在會擲回例外狀況的佈景主題: 例外狀況的預定收件者是程式設計師而非程式的使用者。

本資料大多來自我的著作的下一期的附註 」 基本 C# 6.0 (第 5 版) 」 (Addison-wesley-Wesley,2015年),現在可在 itl.tc/EssentialCSharp


Mark Michaelis是的 IntelliTect,他擔任其技術架構設計人員和培訓講師的創辦人。近二十他 Microsoft MVP 和 Microsoft 區域經理自已 2007年。Michaelis 服務於多個 Microsoft 軟體設計檢閱小組,包括 C#、 Microsoft Azure、 SharePoint 和 Visual Studio ALM。他在開發人員會議演說並著述的書籍包括他最新、 「 基本 C# 6.0 (第 5 版)。 」 在 Facebook 上連絡他 facebook.com/Mark.Michaelis, ,他的部落格上 IntelliTect.com/Mark, ,在 Twitter 上: @markmichaelis 或透過電子郵件地 mark@IntelliTect.com

衷心感謝以下技術專家對本文的審閱: Kevin bost<http:、 Jason Peterson 和 Mads Torgerson