例外狀況的最佳做法
適當的例外狀況處理對於應用程式可靠性而言很重要。 您可以刻意處理預期的例外狀況,以防止應用程式當機。 不過,當機的應用程式比未定義行為的應用程式更可靠且可診斷。
本文說明處理和建立例外狀況的最佳做法。
處理例外狀況
下列最佳做法涉及如何處理例外狀況:
- 使用 try/catch/finally 區塊來從錯誤中恢復或釋放資源
- 處理常見狀況以避免例外狀況
- 攔截取消和異步例外狀況
- 設計類別,以避免例外狀況
- 當方法因為例外狀況而無法完成時,還原狀態
- 擷取並重新擲回例外狀況
使用 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,而不是 TaskCanceledException(其衍生自 OperationCanceledException
)。 如果要求取消,許多非同步方法會拋出 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
拋出例外
下列最佳做法涉及拋出例外的方式:
- 使用預先定義的例外狀況類型
- 使用例外狀況產生器方法
- 包含本地化字串訊息
- 使用適當的文法
- 妥善放置 throw 語句
- 不要在 finally 子句中引發例外狀況
- 不要從非預期的地方引發例外狀況,
- 同步擲回自變數驗證例外狀況
使用預先定義的例外狀況類型
只有在預先定義的例外狀況類別不適用時,才會引進新的例外狀況類別。 例如:
- 如果屬性集或方法呼叫不適用於物件的目前狀態,則擲回 InvalidOperationException 例外狀況。
- 如果傳遞無效的參數,請拋出 ArgumentException 例外狀況或其中一個衍生自 ArgumentException的預先定義類別。
注意
雖然最好盡可能使用預先定義的例外狀況類型,但您不應該引發某些 保留 例外狀況類型,例如 AccessViolationException、IndexOutOfRangeException、NullReferenceException 和 StackOverflowException。 如需詳細資訊,請參閱 CA2201:請勿引發保留的例外狀況類型。
使用例外狀況產生器方法
類別通常會在其實作中,從不同位置拋出相同的例外狀況。 若要避免過多的程式代碼,請建立協助程式方法以建立例外狀況並傳回它。 例如:
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
協助方法,這些方法負責分配和拋出例外狀況。 您應該呼叫這些方法,而不是建構並擲回對應的例外狀況類型:
- ArgumentNullException.ThrowIfNull
- ArgumentException.ThrowIfNullOrEmpty(String, String)
- ArgumentException.ThrowIfNullOrWhiteSpace(String, String)
- ArgumentOutOfRangeException.ThrowIfZero<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfNegative<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfLessThan<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfNotEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfNegativeOrZero<T>(T, String)
- ArgumentOutOfRangeException.ThrowIfGreaterThan<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfLessThanOrEqual<T>(T, T, String)
- ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual<T>(T, T, String)
- ObjectDisposedException.ThrowIf
如果您要實作非同步方法,請呼叫 CancellationToken.ThrowIfCancellationRequested(),而不是檢查是否要求取消,然後建構和拋出 OperationCanceledException。 如需詳細資訊,請參閱 CA2250。
包含本地化的字串訊息
使用者看到的錯誤訊息衍生自擲回之例外狀況的 Exception.Message 屬性,而不是衍生自例外狀況類別的名稱。 一般而言,您會將訊息字串傳遞至 Exception 建構函式的 message
自變數,以將值指派給 Exception.Message 屬性。
針對本地化的應用程式,您應該針對應用程式可以擲回的每個例外狀況提供本地化的訊息字串。 您可以使用資源檔案來提供本地化的錯誤訊息。 如需本地化應用程式和擷取本地化字串的相關信息,請參閱下列文章:
使用適當的文法
撰寫清楚的句子,並包含結尾標點符號。 指派給 Exception.Message 屬性之字串中的每個句子都應該以句號結束。 例如,「記錄數據表已溢位。」會使用正確的文法和標點符號。
妥善放置 throw 語句
請將擲出語句放在堆疊追蹤能夠提供幫助的地方。 堆棧追蹤會從拋出例外狀況的語句開始,並在攔截例外狀況的 catch
語句結束。
請勿在 finally 子句中引發例外狀況
請勿在 finally
子句中引發例外狀況。 如需詳細資訊,請參閱程式代碼分析規則 CA2219。
不要從非預期的地方引發例外狀況
某些方法,例如 Equals
、GetHashCode
和 ToString
方法、靜態建構函式和等號運算符,不應該擲回例外狀況。 如需詳細資訊,請參閱程式代碼分析規則 CA1065。
同步拋出參數驗證例外狀況
在工作傳回方法中,您應該先驗證參數,並在進入方法的異步執行部分之前,擲回任何對應的例外狀況,例如 ArgumentException 和 ArgumentNullException。 在方法非同步部分擲回的例外狀況會被儲存在回傳的工作中,並且直到等待該工作時才會顯現出來。 如需詳細資訊,請參閱工作傳回方法中的 例外狀況。
自定義例外狀況類型
下列最佳做法涉及自訂例外狀況類型:
使用 Exception
結束例外狀況類別名稱
當需要自定義例外狀況時,請適當地命名它,並從 Exception 類別衍生它。 例如:
public class MyFileNotFoundException : Exception
{
}
Public Class MyFileNotFoundException
Inherits Exception
End Class
包含三個建構函式
建立自己的例外狀況類別時,至少使用三個通用建構函式:無參數建構函式、接受字串訊息的建構函式,以及接受字串訊息和內部例外狀況的建構函式。
- Exception(),它會使用預設值。
- Exception(String),它會接受字串訊息。
- Exception(String, Exception),它會接受字串訊息和內部例外狀況。
如需範例,請參閱 如何:建立使用者定義的例外狀況。
視需要提供其他屬性
只在額外資訊有用的程序設計情境下,才提供例外狀況的其他屬性(除了自定義訊息字串之外)。 例如,FileNotFoundException 會提供 FileName 屬性。