簡體   English   中英

我應該如何處理 Dispose() 方法中的異常?

[英]How should I handle exceptions in my Dispose() method?

我想提供一個類來管理臨時目錄的創建和后續刪除。 理想情況下,我希望它可以在 using 塊中使用,以確保無論我們如何離開塊,目錄都會再次被刪除:

static void DoSomethingThatNeedsATemporaryDirectory()
{
    using (var tempDir = new TemporaryDirectory())
    {
        // Use the directory here...
        File.WriteAllText(Path.Combine(tempDir.Path, "example.txt"), "foo\nbar\nbaz\n");
        // ...
        if (SomeCondition)
        {
            return;
        }
        if (SomethingIsWrong)
        {
            throw new Exception("This is an example of something going wrong.");
        }
    }
    // Regardless of whether we leave the using block via the return,
    // by throwing and exception or just normally dropping out the end,
    // the directory gets deleted by TemporaryDirectory.Dispose.
}

創建目錄沒有問題。 問題是如何編寫 Dispose 方法。 當我們嘗試刪除目錄時,可能會失敗; 例如,因為我們仍然在其中打開了一個文件。 但是,如果我們允許異常傳播,它可能會屏蔽在 using 塊內發生的異常。 特別是,如果 using 塊內部發生異常,可能是導致我們無法刪除目錄的異常,但是如果我們屏蔽它,我們就丟失了修復問題的最有用的信息。

看來我們有四種選擇:

  1. 嘗試刪除目錄時捕獲並吞下任何異常。 我們可能不知道我們沒有清理臨時目錄。
  2. 以某種方式檢測 Dispose 是否在拋出異常時作為堆棧展開的一部分運行,如果是,則抑制 IOException 或拋出合並 IOException 和拋出的任何其他異常的異常。 甚至可能都不可能。 (我之所以想到這一點,部分是因為可以使用 Python 的上下文管理器,這在很多方面類似於 .NET 的 IDisposable 與 C# 的 using 語句一起使用。)
  3. 永遠不要抑制 IOException 無法刪除目錄。 如果在 using 塊中拋出異常,我們將隱藏它,盡管它很有可能比我們的 IOException 具有更多的診斷價值。
  4. 放棄在 Dispose 方法中刪除目錄。 該類的用戶必須對請求刪除目錄負責。 這似乎並不令人滿意,因為創建該類的很大一部分動機是為了減輕管理此資源的負擔。 也許有另一種方法可以提供此功能而不會很容易搞砸?

這些選項之一顯然是最好的嗎? 是否有更好的方法在用戶友好的 API 中提供此功能?

與其將其視為實現IDisposable的特殊類,不如想想它在正常程序流程方面的樣子:

Directory dir = Directory.CreateDirectory(path);
try
{
    string fileName = Path.Combine(path, "data.txt");
    File.WriteAllText(fileName, myData);
    UploadFile(fileName);
    File.Delete(fileName);
}
finally
{
    Directory.Delete(dir);
}

這應該如何表現? 這是完全相同的問題。 您是否將finally塊的內容保持原樣,從而可能掩蓋try塊中發生的異常,或者您是否將Directory.Delete包裝在其自己的try-catch塊中,吞下任何異常以防止掩蓋原來的?

我認為沒有任何正確答案——事實上,你只能有一個環境異常,所以你必須選擇一個。 但是,.NET Framework 確實開創了一些先例; 一個例子是 WCF 服務代理 ( ICommunicationObject )。 如果您嘗試Dispose出現故障的通道,它會拋出異常並將屏蔽任何已在堆棧中的異常。 如果我沒記錯的話, TransactionScope也可以做到這一點。

當然,WCF 中的這種行為本身就是一個無休止的混亂之源; 大多數人實際上認為它如果沒有損壞就很煩人。 谷歌“WCF 處理掩碼”,你就會明白我的意思。 所以也許我們不應該總是像微軟那樣做事。

就我個人而言,我認為Dispose永遠不應該掩蓋堆棧中已經存在的異常。 using語句實際上是一個finally塊,並且在大多數情況下(總是存在邊緣情況),您也不希望在finally塊中拋出(而不是捕獲)異常。 原因很簡單,就是調試; 當您甚至無法找出應用程序的確切故障所在時,找出問題的根源可能會非常困難 - 特別是在生產中您無法逐步查看源代碼的問題。 我以前一直處於這個位置,我可以自信地說,它會讓你徹底瘋狂。

我的建議是要么在DisposeDispose異常(當然是記錄它),要么實際檢查是否由於異常而處於堆棧展開場景中,並且只有在您知道自己的情況下才處理后續異常會掩蓋他們。 后者的好處是你不吃例外,除非你真的必須; 缺點是您在程序中引入了一些非確定性行為。 又一個取舍。

大多數人可能只會選擇前一個選項,並簡單地隱藏在finally (或using )中發生的任何異常。

最終,我建議最好遵循FileStream作為指導,它等同於選項 3 和 4:在Dispose方法中關閉文件或刪除目錄,並允許作為該操作的一部分發生的任何異常冒泡(有效地吞下任何發生在using塊內的異常)但允許手動關閉資源,而無需使用塊,如果組件的用戶如此選擇。

與 MSDN 的FileStream文檔不同,我建議您詳細記錄如果用戶選擇using語句可能發生的后果。

這里要問的一個問題是調用者是否可以有效地處理異常。 如果用戶無法合理執行任何操作(手動刪除目錄中正在使用的文件?),最好記錄錯誤並忘記它。

為了涵蓋這兩種情況,為什么沒有兩個構造函數(或構造函數的參數)?

public TemporaryDirectory()
: this( false )
{
}

public TemporaryDirectory( bool throwExceptionOnError )
{
}

然后,您可以將有關適當行為的決定推給類的用戶。

一個常見的錯誤是目錄無法刪除,因為其中的文件仍在使用中:您可以存儲未刪除的臨時目錄列表,並允許在程序關閉期間選擇第二次顯式刪除嘗試(例如 TemporaryDirectory.txt)。 TidyUp() 靜態方法)。 如果有問題的目錄列表非空,則代碼可能會強制垃圾收集處理未關閉的流。

您不能依賴可以以某種方式刪除目錄的假設。 與此同時,其他一些進程/用戶/任何可以在其中創建文件的東西。 防病毒軟件可能正忙於檢查其中的文件等。

您可以做的最好的事情是不僅擁有臨時目錄類,而且擁有臨時文件類(將在臨時目錄的using塊內創建。臨時文件類應該(嘗試)刪除Dispose上的相應文件。這這樣你就可以保證至少已經完成了一次清理嘗試。

假設創建的目錄位於系統臨時文件夾中,例如Path.GetTempPath返回的文件夾,那么我將實施Dispose以便在臨時目錄刪除失敗時不引發異常。

更新:我會選擇這個選項,因為操作可能會因為外部干擾而失敗,比如來自另一個進程的鎖,而且由於目錄被放置在系統臨時目錄中,我不會看到拋出異常的優勢.

對該異常的有效響應是什么? 再次嘗試刪除該目錄是不合理的,如果原因是來自另一個進程的鎖定,則說明它不受您的直接控制。

我會說從鎖定文件的析構函數中拋出異常歸結為使用異常報告預期結果 - 你不應該這樣做。

但是,如果發生其他事情,例如變量為空,您可能真的有錯誤,然后異常就很有價值。

如果您預計會鎖定文件,並且您或可能是您的調用者可以對它們執行某些操作,那么您需要將該響應包含在您的類中。 如果您可以回應,那么只需在一次性電話中進行即可。 如果您的來電者可能能夠響應,請向您的來電者提供解決此問題的方法,例如 TempfilesLocked 事件。

要在using語句中使用該類型,您需要實現IDisposable模式。

要創建目錄本身,請使用Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)作為基礎並使用新的 Guid 作為名稱。

暫無
暫無

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

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