動態

詳情 返回 返回

C# 的異常處理 - 動態 詳情

C# 語言的異常處理功能可幫助你處理程序運行時發生的任何意外或異常情況。異常處理使用 try、catch 和 finally 關鍵字來嘗試可能不成功的操作,以便在您認為這樣做是合理時處理異常,並在完成後清理資源。公共語言運行時(CLR)、.NET 或第三方庫或應用程序代碼可以生成異常。異常是通過使用 throw 關鍵字創建的。

在許多情況下,異常可能不是由代碼直接調用的方法引發的,而是由調用堆棧中其他方法進一步引發的。引發異常時,CLR 將展開堆棧,查找具有 catch 特定異常類型的塊的方法,並執行它找到的第一個此類 catch 塊。如果調用堆棧中的任何位置都找不到適當的 catch 塊,它將終止進程並向用户顯示消息。

在以下示例中,方法用於實現完美的整數除法(返回 double),並捕獲相應的錯誤(DivideByZeroException)。如果沒有異常處理,此程序將因未處理 DivideByZeroException 錯誤而終止。

static decimal FF非整除 ( int x , int y )
    {
        switch ( y )
            {
                case 0:
                    {
                        if ( x == 0 ) { throw new DivideByZeroException ( "將產生 NaN……" ); }
                        else if ( x > 0 ) { throw new DivideByZeroException ( "將產生正無窮……" ); }
                        else if ( x < 0 ) { throw new DivideByZeroException ( "將產生負無窮……" ); }
                        break;
                    }
            }
        return (decimal) x / y;
    }

上例中的除法方法將兩個整數參數相除,但返回值是 decimal,且分別對於正數、負數和零除以零做出了判斷,生成相應的 DivideByZeroException。

異常概述

異常具有以下屬性:

  • 異常是最終全都派生自 System . Exception 的類型。
  • 在可能拋出異常的語句周圍使用 try 代碼塊。
  • 在 try 塊中發生異常後,控制流將跳轉到調用堆棧中任意位置存在的第一個關聯的異常處理程序。在 C# 中,關鍵字 catch 用於定義異常處理程序。
  • 如果沒有給定異常的異常處理程序,程序將停止執行並顯示錯誤消息。
  • 除非可以處理異常並讓應用程序一直處於已知狀態,否則不捕獲異常。如果捕獲 System . Exception,使用 throw 代碼塊末尾的 catch 關鍵字重新拋出異常。
  • 如果 catch 塊定義異常變量,則可以使用它來獲取有關所發生的異常類型的詳細信息。
  • 異常可以通過使用 throw 關鍵字由程序顯式生成。
  • 異常對象包含有關錯誤的詳細信息,例如調用堆棧的狀態和錯誤的文本説明。
  • 無論是否引發異常,都會執行塊中的 finally 代碼。使用 finally 塊來釋放資源,例如關閉在 try 塊中打開的任何流或文件。
  • .NET 中的託管異常是在 Win32 結構化異常處理機制的基礎上實現的。

使用異常

在 C# 中,運行時程序中的錯誤通過使用稱為異常的機制傳播到程序。異常由遇到錯誤的代碼引發,由能夠更正錯誤的代碼捕捉。異常可由 .NET 運行時或由程序中的代碼引發。一旦引發異常,異常會沿調用堆棧向上傳播,直到找到處理異常的語句。未捕獲的異常由顯示對話框的系統提供的泛型異常處理程序處理。

異常由派生自 Exception 的類表示。此類標識異常的類型,幷包含詳細描述異常的屬性。引發異常涉及創建異常派生類的實例,可以選擇配置異常的屬性,然後使用 throw 關鍵字引發對象。例如:

class YC自定義 : Exception
    {
        public YC自定義 ( string 信息 )
            {
            }
    }
private static void TestThrow ( )
    {
        throw new YC自定義 ( "測試中的自定義異常" );
    }

引發異常後,運行時會檢查當前語句,以查看它是否位於 try 塊中。如果在,則將檢查與 try 塊關聯的所有 catch 塊,以確定它們是否可以捕獲該異常。Catch 塊通常指定異常類型;如果 catch 塊的類型與異常的類型相同,或者是該異常的基類,則 catch 塊可以處理該方法。 例如:

try
    {
        TestThrow ( );
    }
catch ( YC自定義 y )
    {
        System . Console . WriteLine ( y . ToString ( ) ); // 測試中的自定義異常
    }

如果引發異常的語句不在 try 塊內,或者將其括起來的 try 塊沒有相應的 catch 塊,運行時會檢查該語句的調用方法中是否包含 try 語句和 catch 塊。運行時繼續調用堆棧,搜索兼容的 catch 塊。在 catch 塊被找到並執行後,控制權然後傳遞給該 catch 塊後面的下一個語句。

語句 try 可以包含多個 catch 塊。執行可以處理異常的第一個 catch 語句;即使這些語句兼容,也會忽略以下 catch 任何語句。按從最具有針對性(或派生程度最高)到最不具有針對性的順序對 catch 塊排列。例如:

try
    {
        using ( StreamWriter sw = new ( "./test.txt" ) )
            {
                sw . WriteLine ( "你好啊……" );
            }
    }
catch ( DirectoryNotFoundException yc )
    {
        Console . WriteLine ( yc . Message );
    }
catch ( FileNotFoundException yc )
    {
        Console . WriteLine ( yc . Message );
    }
catch ( IOException yc )
   {
        Console . WriteLine ( yc . Message );
    }
Console . WriteLine ( "完成……" );

在執行 finally 塊之前,運行時會檢查 catch 塊。finally 塊使程序員能夠清理可能因中止 try 塊而留下的任何不明確狀態,或釋放任何外部資源(如圖形句柄、數據庫連接或文件流),無需等待運行時的垃圾回收器來最終處理對象。例如:

FileStream? wj = null;
FileInfo wj信息 = new ( "./file.txt" );

try
    {
        wj = wj信息 . OpenWrite ( );
        wj . WriteByte ( 0xF );
    }
finally
    {
        wj? . Close ( );
    }

try
    {
        wj = wj信息 . OpenWrite ( );
        wj . WriteByte ( 0x32 );
        Console . WriteLine ( "OpenWrite ( ) 成功" );
    }
catch ( IOException yc )
    {
        Console . WriteLine ( $"OpenWrite ( ) 失敗:{yc . ToString ( )}" );
    }
finally
    {
        wj? . Close ( );
    }

如果 WriteByte ( ) 引發異常,則嘗試重新打開文件的第二個 try 塊中的代碼在未調用的情況下 file . Close ( ) 會失敗,並且該文件將保持鎖定狀態。 由於 finally 塊會被執行,即使引發異常,因此上一示例中的 finally 塊允許文件正確關閉,以幫助避免錯誤。

如果在引發異常後在調用堆棧上找不到兼容的 catch 塊,則會發生以下三種情況之一:

  • 如果異常位於終結器(finalizer)中,則終止終結器,並調用基終結器(如果有)。
  • 如果調用堆棧包含 static 構造函數或 static 字段初始值設定項,則會引發一個 TypeInitializationException(類型初始化異常),並將原始異常賦值給新異常的 InnerException 屬性。
  • 如果到達線程的起始位置,線程就會被終止。

異常處理

C# 程序員使用 try 塊對可能受異常影響的代碼進行分區。關聯的 catch 塊用於處理任何生成的異常。finally 塊包含必定運行的代碼,無論 try 塊中是否拋出異常,該代碼都會執行,比如釋放在 try 塊中分配的資源。try 塊可以沒有一個或多個關聯的 catch 塊;或者一個 finally 塊;或兩者兼有;但不能都沒有。

以下示例演示語句 try……catch 、 try……finally 語句和 try……catch……finally 語句。

try
    {
        // 請在此處編寫嘗試的代碼
    }
catch ( 特定的某種異常 ex )
    {
        // 請在此處編寫處理異常的代碼
        // 僅捕獲您能夠處理的異常
        // 請勿在未在捕獲塊的末尾重新拋出的情況下捕獲基類 “System . Exception”
    }

try
    {
        // 請在此處編寫嘗試的代碼
    }
finally
    {
        // 在 “try” 塊之後要執行的代碼請寫在這裏
    }

try
    {
        // 請在此處編寫嘗試的代碼
    }
catch ( 特定的某種異常 ex )
    {
        // 用於處理異常的代碼請放在此處
    }
finally
    {
        // 在 “try”(以及可能的 “catch”)塊之後要執行的代碼
        // 請在此處編寫
    }

缺少 catch 塊和 finally 塊的 try 塊會導致編譯器錯誤。

catch 塊

catch 塊可以指定要捕獲的異常類型。這種類型的規定稱為異常篩選器。異常類型應派生自 Exception 類。一般情況下,請勿將 Exception 指定為異常篩選器,除非你知道如何處理可能在 try 代碼塊中引發的所有異常,或者已在 catch 代碼塊末尾包含 throw 語句。

可以將具有不同 Exception 類的多個 catch 塊串聯起來。在您的代碼中,這些 catch 塊是從上到下依次被評估的,但對於每個拋出的 Exception,只會執行一個 catch 塊。首先執行的是那個明確指定了拋出 Exception 的確切類型或其基類的 catch 塊。如果沒有 catch 塊指定了與拋出 Exception 類相匹配的類型,則會選擇在該語句中存在且沒有指定任何類型的 catch 塊(如果有的話)。重要的是要將具有最具體(即最派生)Exception 類的 catch 塊放在最前面。

當以下條件為 true 時,捕獲異常:

  • 你可以很好地瞭解引發 Exception 的原因,並且可以實現特定的恢復,例如在捕獲 FileNotFoundException 對象時提示用户輸入新文件名。
  • 可以創建和引發(throw)一個新的、更具體的 Exception。

    int FF獲取索引值 ( int [ ] 數組 , int 索引 )
      {
          try
              {
                  return 數組 [ 索引 ];
              }
          catch ( IndexOutOfRangeException yc )
              {
                  throw new ArgumentOutOfRangeException ( "索引參數超出範圍。" , yc );
              }
      }
  • 想要先對異常進行部分處理,然後再將其傳遞以進行更多處理。在以下示例中, catch 塊用於在重新引發異常之前將條目添加到錯誤日誌。

    static void Main(string[] args)
      {
          try
              {
                  // 嘗試的代碼
              }
          catch ( UnauthorizedAccessException yc )
              {
                  LogError ( yc );
                  throw;
              }
      }
    
    public static void LogError ( Exception yc )
      {
          using ( StreamWriter sw = new ( "./Error.txt" ) )
              {
                  sw . WriteLine ( $"{DateTime . Now}→{yc . Message}" );
              };
      }

還可以指定異常篩選器 ,將布爾表達式添加到 catch 子句。異常篩選器指示僅當該條件為 true 時,特定的 catch 子句才匹配。在以下示例中,這兩個 catch 子句使用相同的 Exception 類,但會檢查額外的條件以創建不同的錯誤消息:

int FF獲取索引值 ( int [ ] 數組 , int 索引 )
    {
        try
            {
                return 數組 [ 索引 ];
            }
        catch ( IndexOutOfRangeException yc ) when ( 索引 < 0 )
            {
                throw new ArgumentOutOfRangeException ( 
"索引參數不能是負數。" , yc );
            }
        catch ( IndexOutOfRangeException yc )
            {
                throw new ArgumentOutOfRangeException (
"索引參數不能大於數組大小," , yc );
            }
    }

始終返回 false 的異常篩選器可用於檢查所有異常,但不能處理這些異常。典型的用途是記錄異常:

public class ExceptionFilter
    {
        public static void Main ( )
            {
                try
                    {
                        string? s = null;
                        Console . WriteLine ( s . Length );
                    }
                catch ( Exception e ) when ( LogException ( e ) )
                    {
                    }
                Console . WriteLine ( "必須已經處理了異常情況" );
        }

    private static bool LogException ( Exception e )
        {
            Console . WriteLine ( $"\t在日誌記錄程序中。 捕獲到 {e . GetType ( )}" );
            Console . WriteLine ( $"\t信息:{e . Message}" );
            return false;
        }
    }

LogException 方法始終返回 false,使用此異常篩選器的 catch 子句均不匹配。catch 子句可以是通用的,使用 System . Exception,後面的子句可以處理更具體的異常類。

finally 塊

使用 finally 塊可以清理在 try 塊中執行的操作。如果存在,finally 塊將在 try 塊和任何匹配的 catch 塊之後最後執行。無論是否會引發異常或找到匹配異常類型的 catch 塊,finally 塊都將始終運行。

finally 塊可用於釋放文件流、數據庫連接和圖形句柄等資源,而無需等待運行時中的垃圾回收器完成這些對象。

在以下示例中,該 finally 塊用於關閉在 try 塊中打開的文件。請注意,在關閉文件之前,將檢查文件句柄的狀態。如果 try 塊無法打開文件,則文件句柄仍具有值 null,並且 finally 塊不會嘗試關閉它。相反,如果文件在 try 塊中成功打開,則 finally 塊會關閉打開的文件。

FileStream? wjl = null;
FileInfo wj = new ( "./file.txt" );
try
    {
        wjl = wj . OpenWrite ( );
        wjl . WriteByte ( 0xF );
    }
finally
    {
        // 檢查 null,因為 OpenWrite 可能是失敗的
        wjl? . Close ( );
    }

創建和引發異常

Exception 用於表示在運行程序時發生了錯誤。描述錯誤的 Exception 對象會被創建出來,然後通過 throw 語句或表達式進行拋出。隨後,運行時系統會搜索最匹配的異常處理程序。

當以下一種或多種情況成立時,程序員應當拋出異常:

  • 該方法無法實現其預定的功能。例如,如果某個方法的參數具有無效值:

    static void FF複製對象 ( Lei例子 源 )
      {
          _ = 源 ?? throw new ArgumentException ( "參數不能是 null" , nameof ( 源 ) );
      }
  • 根據對象的狀態,對對象進行了不恰當的操作調用。例如,可能會嘗試向只讀文件寫入數據。如果對象的狀態不允許執行某項操作,則應拋出一個實例化的 “InvalidOperationException” 對象,或者基於此類的派生類來拋出對象。以下代碼是一個拋出 “InvalidOperationException” 對象的方法示例:

    public class Log
      {
          FileStream wjLog = null!;
          public void FF打開Log ( FileInfo 文件名 , FileMode 模式 ) { }
    
          public void FF寫入Log ( )
              {
                  if ( !logFile . CanWrite )
                      {
                          throw new InvalidOperationException ( "日誌文件不能是隻讀的" );
                      }
                  // 寫入日誌文件的代碼並返回
              }
      }
  • 當調用方法時引發異常時。在這種情況下,應捕獲原始的異常,並創建一個 ArgumentException 實例。原始的異常應作為 InnerException 參數傳遞給 ArgumentException 的構造函數:

    static int FF獲取索引值 ( int [ ] 數組 , int 索引 )
      {
          try
              {
                  return 數組 [ 索引 ];
              }
          catch ( IndexOutOfRangeException yc )
              {
                  throw new ArgumentOutOfRangeException ( 
    "索引參數不在範圍內" , yc );
              }
      }

注意:上述示例展示瞭如何使用 InnerException 屬性。這裏的內容是經過簡化處理的。實際應用中,您應該在使用索引之前先檢查其是否在有效範圍內。當調用某個參數的成員時,如果該成員拋出了您在調用該成員之前無法預料到的異常,您可以採用這種將異常進行封裝的技術。

Exception 對象包含一個名為 “StackTrace” 的屬性。該字符串包含了當前調用棧中各個方法的名稱,以及每個方法拋出異常時所在的文件名和行號。通用語言運行時(CLR)會自動根據拋出語句的位置創建一個 “StackTrace” 對象,因此異常必須從應該開始顯示堆棧跟蹤的位置進行拋出。

所有 Exception 對象都包含一個名為 “Message” 的屬性。此字符串應被設置為用於解釋異常發生原因的説明文字。任何與安全性相關的敏感信息都不應包含在消息文本中。除了 “Message” 之外,異常參數(ArgumentException)還包含一個名為“ParamName”的屬性,該屬性應設置為導致異常拋出的參數的名稱。在屬性設置器中,ParamName 應設置為值。

public 方法和 protected 方法在無法完成預期功能時都會拋出異常。拋出的異常類應是最符合錯誤情況的最具體類型的異常。這些異常應作為類功能的一部分進行記錄,並且派生類或對原始類的更新應保持相同的行為,以確保向後兼容性。

拋出異常時需考慮的事項

以下列表列出了在拋出異常時應考慮的事項:

  • 不要將異常用作常規執行過程中的程序流程變更手段。應利用異常來報告和處理錯誤情況。
  • 異常不應作為返回值或參數返回,而應直接拋出。
  • 切勿在自己的源代碼中有意地拋出 System . Exception、System . SystemException、System . NullReferenceException 或 System . IndexOutOfRangeException 異常。
  • 不要創建在 debug(調試)模式下可拋出但在 release(發佈)模式下不可拋出的異常。在開發階段,若要識別運行時錯誤,請使用 Debug Assert。

任務返回方法中的異常

使用 async 修飾符聲明的方法在處理異常時有一些特殊注意事項。在異步方法中拋出的異常會存儲在返回的任務中,並且不會立即顯現,例如在 await 任務時才會顯現。

我們建議您在進入方法的異步部分之前先驗證參數並拋出相應的異常,例如 ArgumentException 和 ArgumentNullException。也就是説,這些驗證異常應當在工作開始之前同步出現。以下代碼片段展示了一個示例,在該示例中,如果拋出異常,那麼 ArgumentException 異常會同步出現,而 InvalidOperationException 則會存儲在返回的任務中。

// 非異步,方法返回 task
// 在方法內部(但不在局部函數中),所有拋出的 Exception 都會同步出現

public static Task < 烤 > 麪包Async ( int 片 , int 烤制時間 )
    {
        if ( 片 is < 1 or > 4 )
        {
            throw new ArgumentException ( "你必須指定烤制 1 到 4 片面包。" , nameof ( 片 ) );
        }

        if ( 烤制時間 < 1 )
            {
                throw new ArgumentException ( "烤制時間太短。" , nameof ( 烤制時間 ) );
            }

        return FF烤麪包 ( 片 , 烤制時間 );

    // 本地 async 方法
    // 在這個方法中,所有異常存儲於 task 中

    static async Task < 烤 > FF烤麪包 ( int 片 , int 烤制時間 )
        {
            for ( int p = 0 ; p < 片 ; p++ )
                {
                    Console . WriteLine ( "將一片面包放入烤麪包機中");
                }
            // 開始烤制
            await Task . Delay ( 烤制時間 );

            if ( 烤制時間 > 2_000 )
                {
                    throw new InvalidOperationException ( "麪包機着火了!" );
                }

            Console . WriteLine ( "麪包機準備好了!" );

            return new 烤 ( );
        }
    }

定義異常類

程序可以在 “System” 命名空間中拋出預定義的異常類(除非有特別説明),或者通過從 “Exception” 類派生來創建自己的異常類。派生類應至少定義三個構造函數:一個無參數的構造函數、一個設置 “Message” 屬性的構造函數以及一個同時設置 “Message” 和 “InnerException” 屬性的構造函數。例如:

[Serializable]
public class YC有效的 : Exception
    {
        public YC有效的 ( ) : base ( ) { }
        public YC有效的 ( string 信息 ) : base ( message ) { }
        public YC有效的 ( string 信息 , Exception 內部) : base ( 信息 , 內部 ) { }
    }

當所提供的數據有助於解決異常情況時,應向異常類添加新的屬性。如果向派生的異常類添加了新的屬性,則應重寫 ToString ( ) 方法以返回添加的信息。

編譯器生成的異常

當基本操作失敗時,.NET 運行時會自動拋出一些異常。這些異常及其錯誤情況列在以下表格中。

異常 描述
算術異常 這是在進行算術運算時發生的異常的基類,例如除以零異常和溢出異常
數組類型不匹配異常 當一個數組無法存儲給定的元素,因為該元素的實際類型與數組的實際類型不兼容時拋出此異常
除以零異常 當嘗試將一個整數值除以零時拋出此異常
索引超出範圍異常 當嘗試對數組進行索引操作,而索引小於零或超出數組的邊界時拋出此異常
無效轉換異常 在運行時,當從基類型顯式轉換為接口或派生類型時失敗時拋出此異常
null 引用異常 當嘗試引用值為 null 的對象時拋出此異常
內存不足異常 當使用 new 操作符分配內存時失敗時拋出此異常。此異常表示通用語言運行時(CLR)可用的內存已耗盡
溢出異常 在受檢查的算術操作中溢出時拋出此異常
棧溢出異常 當由於有太多未完成的方法調用而導致執行棧耗盡時拋出此異常;通常表示一種非常深或無限的遞歸
類型初始化異常 當靜態構造函數拋出異常且不存在與之匹配的捕獲語句來捕獲該異常時而拋出此異常

Add a new 評論

Some HTML is okay.