例外狀況的最佳做法

設計良好的應用程式可處理例外狀況和錯誤,防止應用程式損毀。 本文將說明處理和建立例外狀況的最佳實務作法。

使用 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)
{
    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 (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

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

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

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

設計類別以避免例外狀況

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

class FileRead
{
public:
    void ReadAll(FileStream^ fileToRead)
    {
        // This if statement is optional
        // as it is very unlikely that
        // the stream would ever be null.
        if (fileToRead == nullptr)
        {
            throw gcnew System::ArgumentNullException();
        }

        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 void ReadAll(FileStream fileToRead)
    {
        // This if statement is optional
        // as it is very unlikely that
        // the stream would ever be null.
        if (fileToRead == null)
        {
            throw new ArgumentNullException();
        }

        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> 還是預設作為錯誤指標,都是要針對您的應用程式考慮的事項。 透過使用 Nullable<Guid>default 會成為 null,而非 Guid.Empty。 有時候,當值存在或不存在時,新增 Nullable<T> 可能會更清楚。 其他時候,新增 Nullable<T> 可能會建立額外的案例,以檢查不需要的情況,並只用來建立潛在的錯誤來源。

擲回例外狀況來代替傳回錯誤碼

例外狀況可確保失敗不會注意到,因為呼叫程式碼未檢查傳回碼。

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

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

使用字組 Exception 作為例外狀況類別名稱的結尾

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

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

在自訂例外狀況類別中包含三個建構函式

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

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

確保從遠端執行程式碼時可以使用例外狀況資料

當您建立使用者定義的例外狀況時,請確定例外狀況的中繼資料可供遠端執行的程式碼使用。

例如,在支援應用程式域的 .NET 實作上,可能會跨應用程式域發生例外狀況。 假設應用程式域 A 會建立應用程式域 B,它會執行擲回例外狀況的程式碼。 若要讓應用程式域 A 正確攔截並處理例外狀況,它必須能夠尋找包含應用程式域 B 所擲回例外狀況的元件。如果應用程式域 B 在其應用程式基底下的元件中擲回例外狀況,但在應用程式域 A 的應用程式基底下,應用程式域 A 將無法找到例外狀況,而 Common Language Runtime 將會擲 FileNotFoundException 回例外狀況。 若要避免這種情況,您可以使用下列兩種方式之一部署包含例外狀況資訊的元件:

  • 將組件放入這兩個應用程式定義域共用的通用應用程式基底。
  • 如果網域未共用通用應用程式基底,請使用強式名稱簽署包含例外狀況資訊的元件,並將元件部署至全域組件快取。

使用文法正確的錯誤訊息

撰寫清楚的句子並包含結尾標點符號。 在每個指派給 Exception.Message 屬性的字串中之句子,都應以句點結束。 例如,「記錄資料表已溢位」是適當的訊息字串。

在每個例外狀況中,納入當地語系化的字串訊息

使用者看到的錯誤訊息衍生自 Exception.Message 擲回之例外狀況的 屬性,而不是衍生自例外狀況類別的名稱。 一般而言,您會將訊息字串傳遞至Exception 建構函式的引數,將值指派給 Exception.Messagemessage 屬性。

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

在自訂例外狀況中,視需要提供額外的屬性

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

放置 throw 陳述式讓堆疊追蹤更有用

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

使用例外狀況產生器方法

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

ref class FileReader
{
private:
    String^ fileName;

public:
    FileReader(String^ path)
    {
        fileName = path;
    }

    array<Byte>^ Read(int bytes)
    {
        array<Byte>^ results = FileUtils::ReadFromFile(fileName, bytes);
        if (results == nullptr)
        {
            throw NewFileIOException();
        }
        return results;
    }

    FileReaderException^ NewFileIOException()
    {
        String^ description = "My NewFileIOException Description";

        return gcnew FileReaderException(description);
    }
};
class FileReader
{
    private string fileName;

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

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

    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

在某些情況下,使用例外狀況的建構函式來建置例外狀況會更適當。 範例為全域例外狀況類別 ArgumentException

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

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

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

擷取稍後要重新擲回的例外狀況

若要擷取例外狀況並保留其呼叫堆疊,以便稍後重新擲回例外狀況,請使用 System.Runtime.ExceptionServices.ExceptionDispatchInfo 類別。 這個類別提供下列方法與屬性, (其他) :

下列範例示範如何使用 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

另請參閱