簡體   English   中英

如何在 C# 中捕獲原始(內部)異常?

[英]How to catch the original (inner) exception in C#?

我正在調用一個引發自定義異常的 function:

GetLockOwnerInfo(...)

這個 function 又調用了一個拋出異常的 function:

GetLockOwnerInfo(...)
   ExecuteReader(...)

這個 function 又調用了一個拋出異常的 function:

GetLockOwnerInfo(...)
   ExecuteReader(...)
      ExecuteReader(...)

等等:

GetLockOwnerInfo(...)
   ExecuteReader(...)
      ExecuteReader(...)
         ExecuteReaderClient(...)
             Fill(...)

其中一個函數會引發SqlException ,盡管該代碼不知道SqlException是什么。

更高級別將該SqlException包裝到另一個BusinessRuleException中,以包含一些特殊屬性和其他詳細信息,同時將“原始”異常包括為InnerException

catch (DbException ex)
{
    BusinessRuleExcpetion e = new BusinessRuleException(ex)
    ...
    throw e;
}

更高級別將該BusinessRuleException包裝到另一個LockerException中,以包含一些特殊屬性和其他詳細信息,同時將“原始”異常包含為InnerException

catch (BusinessRuleException ex)
{
    LockerException e = new LockerException(ex)
    ...
    throw e;
}

現在的問題是我想捕獲原始的SqlException ,以檢查特定的錯誤代碼。

但是沒有辦法“捕捉到內部異常”:

try
{
   DoSomething();
}
catch (SqlException e)
{
   if (e.Number = 247) 
   {
      return "Someone";
   }
   else
      throw;
}

我想過在SqlException時立即捕獲它,並將各種值復制到重新拋出的異常中 - 但該代碼不依賴於 Sql。 它正在遇到SqlException ,但它不依賴於 SqlException。

我想過捕獲所有異常:

try
{
   DoSomething(...);
}
catch (Exception e)
{
   SqlException ex = HuntAroundForAnSqlException(e);
   if (ex != null)
   {
      if (e.Number = 247) 
      {
          return "Someone";
      }
      else
         throw;
   }
   else
      throw;
}

但那是可怕的代碼。

鑒於 .NET 不允許您更改Exception Message以包含其他信息,那么捕獲原始異常的預期機制是什么?

您需要 c# 6 / Visual Studio 2015 才能使用謂詞執行此操作:

catch (ArgumentException e) when (e.ParamName == “…”)
{
}

官方 C# Try/Catch 文檔

我不想告訴你這一點,但你無法捕捉到內在的異常。

你能做的就是檢查一個。

我建議您捕獲高級異常(我相信它是LockerException )並檢查該InnerException屬性。 檢查類型,如果它不是SqlException ,請檢查該InnerException 遍歷每一個,直到找到SqlException類型,然后獲取所需的數據。

也就是說,我同意 dasblinkenlight 您應該考慮——如果可能的話——對您的異常框架進行大量重構。

檢查包裝異常的錯誤代碼不是一個好習慣,因為它嚴重損害了封裝。 想象一下,在某個時刻重寫邏輯以從非 SQL 源(例如 Web 服務)讀取數據。 在相同的條件下它會拋出SQLException以外的東西,你的外部代碼將無法檢測到它。

您應該將代碼添加到捕獲SQLException的塊中,以立即檢查e.Number = 247 ,並拋出帶有一些屬性的BusinessRuleException以區別於響應非SQLExceptionSQLException引發的BusinessRuleExceptione.Number != 247 in一些有意義的方式。 例如,如果幻數247表示您遇到了重復項(此時我純屬猜測),您可以執行以下操作:

catch (SQLException e) {
    var toThrow = new BusinessRuleException(e);
    if (e.Number == 247) {
        toThrow.DuplicateDetected = true;
    }
    throw toThrow;
}

當您稍后捕獲BusinessRuleException ,您可以檢查其DuplicateDetected屬性,並采取相應的行動。

編輯 1 (響應 DB 讀取代碼無法檢查SQLException

您還可以更改BusinessRuleException以檢查其構造函數中的SQLException ,如下所示:

public BusinessRuleException(Exception inner)
:   base(inner) {
    SetDuplicateDetectedFlag(inner);
}

public BusinessRuleException(string message, Exception inner)
:   base(message, inner) {
    SetDuplicateDetectedFlag(inner);
}

private void SetDuplicateDetectedFlag(Exception inner) {
    var innerSql = inner as SqlException;
    DuplicateDetected = innerSql != null && innerSql.Number == 247;
}

這是不太可取的,因為它破壞了封裝,但至少它在一個地方做到了。 如果您需要檢查其他類型的異常(例如,因為您添加了一個 Web 服務源),您可以將其添加到SetDuplicateDetectedFlag方法中,然后一切都會再次運行。

讓外部應用層關心包裝異常的細節是一種代碼味道; 包裹得越深,氣味越大。 你現在已經包裹類SqlExceptiondbException據推測設計而露出的SqlClient作為一個通用的數據庫接口。 因此,該類應包括區分不同例外情況的方法。 例如,它可以定義一個 dbTimeoutWaitingForLockException 並決定在捕獲 SqlException 時拋出它,並根據其錯誤代碼確定存在鎖定超時。 在 vb.net 中,擁有一個公開 ErrorCause 枚舉的 dbException 類型可能會更清晰,因此可以說Catch Ex as dbException When ex.Cause = dbErrorCauses.LockTimeout ,但不幸的是異常過濾器在 C# 中不可用。

如果有一種情況,內部類包裝器不知道它在做什么以知道它應該如何映射異常,那么讓內部類方法接受一個將接受異常的異常包裝委托可能會有所幫助內部類已經捕獲或“想要”拋出,並以適合外部類的方式包裝它。 在直接從外部類調用內部類的情況下,這種方法可能會有些過頭,但如果涉及中間類,則可能會很有用。

好問題和好答案!

我只想用一些進一步的想法來補充已經給出的答案:

一方面,我同意 dasblinkenlight 和其他用戶的觀點。 如果您捕獲一個異常以重新拋出不同類型的異常,並將原始異常設置為內部異常,那么除了維護方法的約定之外,您不應出於其他原因執行此操作。 (訪問 SQL 服務器是調用者不知道/必須不知道/不能知道的實現細節,因此它無法預期會SqlException DbException (或DbException )。)

然而,應用這種技術有一些應該注意的含義:

  • 您隱藏了錯誤的根本原因。 在您的示例中,您向調用者報告業務規則無效(?),違反(?)等,而實際上訪問數據庫時出現問題(如果允許DbException冒泡,這將立即清楚調用堆棧進一步)。
  • 您正在隱藏最初發生錯誤的位置。 捕獲異常的StackTrace屬性將指向遠離最初發生錯誤位置的捕獲塊。 除非您非常小心地記錄所有內部異常的堆棧跟蹤,否則這會使調試變得非常困難。 (尤其是在將軟件部署到生產環境中並且您無法附加調試器時更是如此……)

鑒於 .NET 不允許您更改異常消息以包含附加信息,那么捕獲原始異常的預期機制是什么?

.NET 確實不允許您更改異常消息。 然而,它提供了另一種機制來通過Exception.Data字典向異常提供附加信息。 因此,如果您只想向異常添加額外數據,則沒有理由包裝原始異常並拋出新異常。 而是只做:

public void DoStuff(String filename)
{
    try {
       // Some file I/O here...
    }
    catch (IOException ex) {

      // Add filename to the IOException
      ex.Data.Add("Filename", filename);

      // Send the exception along its way
      throw;
    }
}

正如其他人所說,您無法捕捉到 InnerException。 像這樣的函數可以幫助您從樹中獲取 InnerException :

public static bool TryFindInnerException<T>(Exception top, out T foundException) where T : Exception
{
    if (top == null)
    {
        foundException = null;
        return false;
    }

    Console.WriteLine(top.GetType());
    if (typeof(T) == top.GetType())
    {
        foundException = (T)top;
        return true;
    }

    return TryFindInnerException<T>(top.InnerException, out foundException);
}

我同意其他評論,即這是一種代碼味道,應該避免。 但是如果重構是不可能的,你可以嘗試這樣的事情......

創建擴展方法...

public static bool HasInnerException(this Exception ex, Func<Exception, bool> match)
{
   if (ex.InnerException == null)
   {
      return false;
   }

   return match(ex.InnerException) || HasInnerException(ex.InnerException, match);
}

並像使用它一樣...

catch (Exception ex) when (ex.HasInnerException(e => e is MyExceptionThatIsHidden))
{ 
   ...

但實際上你應該解決

var exception = new Exception("wrapped exception 3",
   new Exception("wrapped exception 2",
      new Exception("wrapped exception 1",
         new MyExceptionThatIsHidden("original exception")))); // <--- ???

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM