分享方式:


例外狀況的最佳做法

適當的例外狀況處理對於應用程式可靠性至關重要。 為了防止您的應用程式損毀,您可以刻意處理預期的例外狀況。 然而,損毀的應用程式比具有未定義行為的應用程式更可靠且可診斷。

本文將說明處理和建立例外狀況的最佳作法。

處理例外狀況

下列最佳做法涉及如何處理例外狀況:

使用 try/catch/finally 區塊從錯誤中復原或釋放資源

對可能產生例外狀況的程式碼,以及當應用程式可從該例外狀況復原時,請在程式碼周圍使用 try/catch 區塊。 在 catch 區塊中,一律將例外狀況從最具衍生性的排列到最不具衍生性。 (所有例外狀況皆衍生自 Exception 類別。前有基底例外狀況類別 catch 子句的 catch 子句,不會處理最具衍生性的例外狀況。)當您的程式碼無法從例外狀況復原時,請勿攔截該例外狀況。 如果可能,請啟用方法讓呼叫堆疊盡可能修復。

清除配置了 using 陳述式或 finally 區塊的資源。 擲回例外狀況時,偏好使用 using 陳述式來自動清除資源。 使用 finally 區塊清除不會實作 IDisposable 的資源。 就算擲回例外狀況,也一律執行 finally 子句中的程式碼。

處理常見情況以避免例外狀況

針對可能發生但可能會觸發例外狀況的情況,請考慮以避免例外狀況的方式來處理這些情況。 例如,如果您嘗試關閉已關閉的連線,您會得到 InvalidOperationException。 您可以在嘗試關閉之前,使用 if 陳述式來檢查連線狀態,以避免此例外狀況。

if (conn.State != ConnectionState.Closed)
{
    conn.Close();
}
If conn.State <> ConnectionState.Closed Then
    conn.Close()
End IF

如果您沒有檢查連線狀態就關閉,可能會攔截到 InvalidOperationException 例外狀況。

try
{
    conn.Close();
}
catch (InvalidOperationException ex)
{
    Console.WriteLine(ex.GetType().FullName);
    Console.WriteLine(ex.Message);
}
Try
    conn.Close()
Catch ex As InvalidOperationException
    Console.WriteLine(ex.GetType().FullName)
    Console.WriteLine(ex.Message)
End Try

選擇的方法取決於您預期事件會發生的頻率。

  • 如果事件不常發生,也就是說事件確實是例外且表示錯誤 (例如未預期的檔案結尾),則使用例外狀況處理。 當您使用例外狀況處理時,在正常情況下會執行較少的程式碼。

  • 如果事件定期發生而且可視為正常執行的一部分,請檢查程式碼中是否有錯誤狀況。 當您檢查是否有常見的錯誤狀況時,因為避免例外狀況,所以會執行較少的程式碼。

    注意

    大多數情況下,預先檢查可以消除例外狀況。 不過,在檢查與作業之間可能會有競爭條件,而受防護的條件可能會發生變化,在此情況下,您仍可能會遇到例外狀況。

呼叫 Try* 方法以避免例外狀況

如果例外狀況的效能成本過高,則某些 .NET 程式庫方法會提供替代形式的錯誤處理。 例如,如果要剖析的值太大而無法以 Int32 表示,則 Int32.Parse 會擲回 OverflowException。 不過,Int32.TryParse 不會擲回此例外狀況。 相反地,其會傳回布林值,並具有 out 參數,其中包含成功時剖析的有效整數。 Dictionary<TKey,TValue>.TryGetValue 在嘗試從字典取得值時,具有類似的行為。

攔截取消和非同步例外狀況

當您呼叫非同步方法時,最好攔截 OperationCanceledException,而不是衍生自 OperationCanceledExceptionTaskCanceledException。 如果要求取消,許多非同步方法會擲回 OperationCanceledException 例外狀況。 一旦觀察到取消要求,這些例外狀況可以有效地停止執行並展開呼叫堆疊。

非同步方法會將執行期間擲回的例外狀況儲存在它們傳回的工作中。 如果例外狀況儲存在傳回的工作中,則在等候工作時會擲回該例外狀況。 使用例外狀況,例如 ArgumentException,仍然會同步擲回。 如需詳細資訊,請參閱非同步執行

設計類別以避免例外狀況

類別可提供方法或屬性,讓您避免進行會觸發例外狀況的呼叫。 例如,FileStream 類別會提供方法,協助判斷是否已經抵達檔案結尾。 您可以呼叫這些方法來避免讀取超過檔案結尾時所擲回的例外狀況。 下列範例示範如何讀取至檔案結尾,而不會觸發例外狀況:

class FileRead
{
    public static void ReadAll(FileStream fileToRead)
    {
        ArgumentNullException.ThrowIfNull(fileToRead);

        int b;

        // Set the stream position to the beginning of the file.
        fileToRead.Seek(0, SeekOrigin.Begin);

        // Read each byte to the end of the file.
        for (int i = 0; i < fileToRead.Length; i++)
        {
            b = fileToRead.ReadByte();
            Console.Write(b.ToString());
            // Or do something else with the byte.
        }
    }
}
Class FileRead
    Public Sub ReadAll(fileToRead As FileStream)
        ' This if statement is optional
        ' as it is very unlikely that
        ' the stream would ever be null.
        If fileToRead Is Nothing Then
            Throw New System.ArgumentNullException()
        End If

        Dim b As Integer

        ' Set the stream position to the beginning of the file.
        fileToRead.Seek(0, SeekOrigin.Begin)

        ' Read each byte to the end of the file.
        For i As Integer = 0 To fileToRead.Length - 1
            b = fileToRead.ReadByte()
            Console.Write(b.ToString())
            ' Or do something else with the byte.
        Next i
    End Sub
End Class

另一個避免例外狀況的方法是針對最常見的錯誤案例傳回 null (或預設值),而不擲回例外狀況。 普遍的錯誤案例可視為一般控制流程。 針對這些案例傳回 null (或預設值),就能盡量降低對應用程式效能的影響。

針對實值型別,請考慮使用 Nullable<T>default 作為應用程式的錯誤指標。 透過使用 Nullable<Guid>default 會成為 null,而非 Guid.Empty。 有時候,當值存在或不存在時,新增 Nullable<T> 可讓您清楚了解。 在其他時候,新增 Nullable<T> 會建立不必要的額外案例,而且會產生潛在的錯誤來源。

當方法因為例外狀況而未完成時還原狀態

呼叫端應該能夠假設,從方法擲回例外狀況時不會產生副作用。 例如,如果您有用來轉帳的程式碼,會從某個帳戶提款再存入另一個帳戶,而且在執行存款時擲回例外狀況,則您不想要讓提款仍有效。

public void TransferFunds(Account from, Account to, decimal amount)
{
    from.Withdrawal(amount);
    // If the deposit fails, the withdrawal shouldn't remain in effect.
    to.Deposit(amount);
}
Public Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
    from.Withdrawal(amount)
    ' If the deposit fails, the withdrawal shouldn't remain in effect.
    [to].Deposit(amount)
End Sub

上述方法不會直接擲回任何例外狀況。 不過,您必須撰寫方法,如此一來,若儲存作業失敗,就會反轉撤銷。

處理這種情況的一個方式是攔截存款交易所擲回的任何例外狀況,並復原提款。

private static void TransferFunds(Account from, Account to, decimal amount)
{
    string withdrawalTrxID = from.Withdrawal(amount);
    try
    {
        to.Deposit(amount);
    }
    catch
    {
        from.RollbackTransaction(withdrawalTrxID);
        throw;
    }
}
Private Shared Sub TransferFunds(from As Account, [to] As Account, amount As Decimal)
    Dim withdrawalTrxID As String = from.Withdrawal(amount)
    Try
        [to].Deposit(amount)
    Catch
        from.RollbackTransaction(withdrawalTrxID)
        Throw
    End Try
End Sub

此範例說明如何使用 throw 重新擲回原始的例外狀況,以方便呼叫端無須檢視 InnerException 屬性,就能輕鬆查看問題的實際原因。 另一種做法是擲回新的例外狀況,並包含原始例外狀況作為內部例外狀況。

catch (Exception ex)
{
    from.RollbackTransaction(withdrawalTrxID);
    throw new TransferFundsException("Withdrawal failed.", innerException: ex)
    {
        From = from,
        To = to,
        Amount = amount
    };
}
Catch ex As Exception
    from.RollbackTransaction(withdrawalTrxID)
    Throw New TransferFundsException("Withdrawal failed.", innerException:=ex) With
    {
        .From = from,
        .[To] = [to],
        .Amount = amount
    }
End Try

適當擷取並重新擲回例外狀況

一旦擲回例外狀況,其中部分資訊為堆疊追蹤。 堆疊追蹤是方法呼叫階層的清單,從擲回例外狀況的方法開始,並以攔截例外狀況的方法作結。 如果您在 throw 陳述式中指定例外來重新擲回例外狀況,例如,throw e,則堆疊追蹤會在目前的方法上重新啟動,且會遺失在擲回例外狀況之原始方法和目前方法之間呼叫的方法清單。 若要保留例外狀況的原始堆疊追蹤資訊,有兩個選項取決於您重新擲回例外狀況的位置:

  • 如果從攔截到例外狀況執行個體的處理常式 (catch 區塊) 中重新擲回例外狀況,請使用 throw 陳述式,而不指定例狀況。 程式代碼分析規則 CA2200 可協助您尋找程式碼中可能不小心遺失堆疊追蹤資訊的位置。
  • 如果您要從處理常式 (catch 區塊) 以外的某個位置重新擲回例外狀況,請使用 ExceptionDispatchInfo.Capture(Exception) 來擷取處理常式中的例外狀況,以及 ExceptionDispatchInfo.Throw() (當您想要重新擲回時)。 可以使用 ExceptionDispatchInfo.SourceException 屬性來檢查所擷取的例外狀況。

下列範例示範如何使用 ExceptionDispatchInfo 類別,以及輸出的外觀。

ExceptionDispatchInfo? edi = null;
try
{
    var txt = File.ReadAllText(@"C:\temp\file.txt");
}
catch (FileNotFoundException e)
{
    edi = ExceptionDispatchInfo.Capture(e);
}

// ...

Console.WriteLine("I was here.");

if (edi is not null)
    edi.Throw();

如果範例程式碼中的檔案不存在,會產生下列輸出:

I was here.
Unhandled exception. System.IO.FileNotFoundException: Could not find file 'C:\temp\file.txt'.
File name: 'C:\temp\file.txt'
   at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)
   at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable`1 unixCreateMode)
   at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)
   at System.IO.File.ReadAllText(String path, Encoding encoding)
   at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 12
--- End of stack trace from previous location ---
   at Example.ProcessFile.Main() in C:\repos\ConsoleApp1\Program.cs:line 24

擲回例外狀況

下列最佳做法涉及如何擲回例外狀況:

使用預先定義的例外狀況類型

只有在預先定義的類型不適用時,才引進新的例外狀況類別。 例如:

注意

儘管在可能的情況下最好使用預先定義的例外狀況類型,但不應該引發某些 reserved 例外狀況類型,例如 AccessViolationExceptionIndexOutOfRangeExceptionNullReferenceExceptionStackOverflowException。 如需詳細資訊,請參閱 CA2201:不要引發保留的例外狀況類型

使用例外狀況產生器方法

類別在其實作中從不同的地方擲回相同的例外狀況是很常見的。 若要避免過多的程式碼,請建立 Helper 方法,以建立例外狀況並將它傳回。 例如:

class FileReader
{
    private readonly string _fileName;

    public FileReader(string path)
    {
        _fileName = path;
    }

    public byte[] Read(int bytes)
    {
        byte[] results = FileUtils.ReadFromFile(_fileName, bytes) ?? throw NewFileIOException();
        return results;
    }

    static FileReaderException NewFileIOException()
    {
        string description = "My NewFileIOException Description";

        return new FileReaderException(description);
    }
}
Class FileReader
    Private fileName As String


    Public Sub New(path As String)
        fileName = path
    End Sub

    Public Function Read(bytes As Integer) As Byte()
        Dim results() As Byte = FileUtils.ReadFromFile(fileName, bytes)
        If results Is Nothing
            Throw NewFileIOException()
        End If
        Return results
    End Function

    Function NewFileIOException() As FileReaderException
        Dim description As String = "My NewFileIOException Description"

        Return New FileReaderException(description)
    End Function
End Class

一些關鍵 .NET 例外狀況類型具有這類靜態 throw 協助程式方法,這些方法會配置和擲回例外狀況。 您應該呼叫這些方法,而不是建構並擲回對應的例外狀況類型:

提示

下列程式代碼分析規則可協助您在程式碼中找到可以利用這些靜態 throw 協助程式的位置:CA1510CA1511CA1512 以及 CA1513

如果您要實作非同步方法,請呼叫 CancellationToken.ThrowIfCancellationRequested(),而不是檢查是否要求取消,然後建構並擲回 OperationCanceledException。 如需詳細資訊,請參閱 CA2250

納入當地語系化的字串訊息

使用者所看到的錯誤訊息,衍生自所擲回例外狀況的 Exception.Message 屬性,而並非來自例外狀況類別的名稱。 一般來說,您要將值指派到 Exception.Message 屬性,方法是將訊息字串傳遞到例外狀況建構函式message 引數。

若是當地語系化的應用程式,則應對每個應用程式可能會擲回的例外狀況,該提供當地語系化的訊息字串。 您可使用資源檔,提供當地語系化的錯誤訊息。 如需當地語系化應用程式與擷取當地語系化字串的詳細資訊,請參閱:

使用適當的文法

撰寫清楚的句子並包含結尾標點符號。 在每個指派給 Exception.Message 屬性的字串中之句子,都應以句點結束。 例如,「記錄資料表已溢位。」會使用正確的文法和標點符號。

適當放置擲回陳述式

將擲回陳述式放在堆疊追蹤有幫助的地方。 堆疊追蹤會從擲回例外狀況所在的陳述式開始,並在攔截例外狀況的 catch 陳述式結束。

請勿在 finally 子句中引發例外狀況

請勿在 finally 子句中引發例外狀況。 如需詳細資訊,請參閱程式碼分析規則 CA2219

請勿從非預期的地點引發例外狀況

一些方法,例如 EqualsGetHashCodeToString 方法、靜態建構函式和等號運算子,不應擲回例外狀況。 如需詳細資訊,請參閱程式碼分析規則 CA1065

同步擲回引數驗證例外狀況

在工作傳回方法中,您應先驗證引數並擲回任何對應的例外狀況,例如 ArgumentExceptionArgumentNullException,再輸入方法的非同步部分。 在方法的非同步部分中擲回的例外狀況會儲存在傳回的工作中,而且要等到等候工作之前才會出現。 如需詳細資訊,請參閱工作傳回方法的例外狀況

自訂例外狀況類型

下列最佳做法涉及自訂例外狀況類型:

Exception 作為例外狀況類別名稱的結尾

如需自訂例外狀況,請適當地加以命名,並從 Exception加以衍生。 例如:

public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
    Inherits Exception
End Class

包含三個建構函式

當您建立自己的例外狀況類別時,請使用至少三個種常見的建構函式:無參數建構函式、採用字串訊息的建構函式,以及採用字串訊息和內部例外狀況的建構函式。

如需範例,請參閱如何:建立使用者定義的例外狀況

視需要提供額外的屬性

只有在其他資訊對某個程式設計案例實用時,才為例外狀況提供其他屬性 (以及自訂訊息字串)。 例如,FileNotFoundException 提供 FileName 屬性。

另請參閱