例外狀況的最佳做法
設計良好的應用程式可處理例外狀況和錯誤,防止應用程式損毀。 本文將說明處理和建立例外狀況的最佳實務作法。
使用 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 例外狀況類型
只有在預先定義的類型不適用時,才引進新的例外狀況類別。 例如:
- 如果屬性集或方法呼叫對於物件的目前狀態而言並不適當,就會擲回 InvalidOperationException 例外狀況。
- 在傳遞的參數無效時擲回 ArgumentException 例外狀況,或擲回預先定義之類別中,從 ArgumentException 衍生而來的類別。
使用字組 Exception
作為例外狀況類別名稱的結尾
如需自訂例外狀況,請適當地加以命名,並從 Exception加以衍生。 例如:
public ref class MyFileNotFoundException : public Exception
{
};
public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
Inherits Exception
End Class
在自訂例外狀況類別中包含三個建構函式
當您建立自己的例外狀況類別時,請使用至少三個種常見的建構函式:無參數建構函式、採用字串訊息的建構函式,以及採用字串訊息和內部例外狀況的建構函式。
- Exception(),會使用預設值。
- Exception(String),它會接受字串訊息。
- Exception(String, Exception),它會接受字串訊息和內部例外狀況。
如需範例,請參閱如何:建立使用者定義的例外狀況。
確保從遠端執行程式碼時可以使用例外狀況資料
當您建立使用者定義的例外狀況時,請確保例外狀況的中繼資料可供遠端執行的程式碼使用。
例如,在支援應用程式域的 .NET 實作上,可能會跨應用程式域發生例外狀況。 假定應用程式定義域 A 建立應用程式定義域 B,其會執行擲回例外狀況的程式碼。 為使應用程式定義域 A 能夠正確地攔截及處理例外狀況,其必須能夠尋找含有應用程式定義域 B 所擲回之例外狀況的組件。若應用程式定義域 B 擲回例外狀況,但此例外狀況包含在位於其應用程式基底之下的組件,而不是位於在應用程式定義域 A 的應用程式基底之下的組件,應用程式定義域 A 將無法尋找例外狀況,而且 Common Language Runtime 將會擲回 FileNotFoundException 例外狀況。 若要避免這個情形,您可以透過兩種方式之一,部署含有例外狀況資訊的組件:
- 將組件放入這兩個應用程式定義域共用的通用應用程式基底。
- 如果網域不共用通用應用程式基底,則以強式名稱簽署含有例外狀況資訊的組件,並將組件部署到全域組件快取中。
使用文法正確的錯誤訊息
撰寫清楚的句子並包含結尾標點符號。 在每個指派給 Exception.Message 屬性的字串中之句子,都應以句點結束。 例如「記錄表溢位」就是適當的訊息字串。
在每個例外狀況中,納入當地語系化的字串訊息
使用者所看到的錯誤訊息,衍生自所擲回例外狀況的 Exception.Message 屬性,而並非來自例外狀況類別的名稱。 一般來說,您要將值指派到 Exception.Message 屬性,方法是將訊息字串傳遞到例外狀況建構函式的 message
引數。
若是當地語系化的應用程式,則應對每個應用程式可能會擲回的例外狀況,該提供當地語系化的訊息字串。 您可使用資源檔,提供當地語系化的錯誤訊息。 如需當地語系化應用程式與擷取當地語系化字串的詳細資訊,請參閱:
在自訂例外狀況中,視需要提供額外的屬性
只有在其他資訊對某個程式設計案例實用時,才為例外狀況提供其他屬性 (以及自訂訊息字串)。 例如,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.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
另請參閱
意見反應
提交並檢視相關的意見反應