簡體   English   中英

D中的慣用錯誤處理

[英]Idiomatic error handling in D

我試圖找到有關處理錯誤的標准化可接受的慣用D方式的資源,但找不到任何東西。 如果您正在閱讀有關錯誤處理的官方文檔,那么您會發現以下非常重要的陳述:

  • 錯誤不是程序正常流程的一部分。 錯誤是異常,異常和意外的。
  • 因為錯誤是異常的,所以錯誤處理代碼的執行對性能並不重要。
  • 程序邏輯的正常流程對性能至關重要。

我之所以稱其為重要的,是因為對此類異常情況使用例外的原因是導致本文得出結論的原因,即錯誤畢竟是特殊情況,而無論花費多少成本,例外都是解決之道。 再次來自同一篇文章:

因為錯誤是異常的,所以錯誤處理代碼的執行對性能並不重要。 異常處理堆棧展開是一個相對緩慢的過程。

在某些特殊情況下,可能無法明確處理異常,但是異常的存在仍然會影響事物的狀態,因此應使用異常安全scope保護器

我的主要問題是,上述解決方案及其文檔中的示例確實是例外情況,當我們遇到與內存相關的問題時,這非常有用,但是我們不希望程序失敗,我們想要維護完整性並在可能的情況下從這些場景中恢復,但是其他情況又如何呢?

正如我們都知道的錯誤不僅可用於特殊情況下的和意想不到的場景,但他們是主叫方被叫方之間的通信方式。 例如,可以在消毒器中使用錯誤。 假設我們要為關聯數組實現模式驗證。 僅類型系統無法定義鍵和值的約束,因此我們創建了一個在運行時檢查此類對象的函數。 那么,如果架構失敗了該怎么辦? 由於我們對它如何失敗很感興趣,因此其中發生的錯誤(即找到的無效數據)也應包含有關錯誤原因的信息,因此調用方將知道如何對其進行操作。 根據第一篇文章的作者,使用異常是一種昂貴的抽象。 根據同一篇文章的同一作者,使用C樣式函數約定將返回值都用於錯誤狀態是錯誤的方式。

那么,處理D中不是異常的錯誤的正確且慣用的方法是什么?

好吧,TLDR版本是使用異常是處理D中錯誤情況的慣用方式,但是當然,細節要比這復雜得多。

問題的一部分是什么構成錯誤。 錯誤一詞用於許多事情,因此,談論錯誤可能會造成很大的混亂。 某些類型的錯誤是程序性錯誤(因此是程序中錯誤的結果),其他類型不是程序性錯誤,但災難性很大,程序無法繼續運行,而其他類型則依賴於諸如用戶輸入之類的東西,並且通常可以被恢復。從。

對於程序錯誤和災難性錯誤,D具有Error類,該類從Throwable派生。 Error兩個常用子類是AssertErrorRangeError - AssertError是斷言失敗的結果,而RangeError是當您嘗試使用越界索引對數組進行索引時得到的結果。 這兩個都是程序錯誤; 它們是程序中錯誤的結果,從它們中恢復是沒有意義的,因為根據定義,此時程序處於無效狀態。 一個不是錯誤的錯誤示例,但通常足以導致程序終止的災難性錯誤是MemoryError ,當new無法分配內存時拋出該錯誤。

引發Error ,不能保證將運行任何清理代碼(例如,可以跳過析構函數和scope語句,因為假設是因為您的代碼處於無效狀態,清理代碼實際上可能使情況變得更糟)。 程序只是展開堆棧,打印出Error的消息和堆棧跟蹤,然后終止程序。 因此,嘗試捕獲Error並讓程序繼續運行幾乎總是一個可怕的主意,因為程序處於未知且無效的狀態。 如果某些東西被認為是Error ,那么它就是這種情況,其中錯誤條件被認為是不可恢復的,程序不應該嘗試從中恢復。

在大多數情況下,您可能不會對Error進行任何明確的操作。 當不使用-release進行編譯時,將在代碼中放入斷言以捕獲錯誤,但是您可能不會明確拋出任何Error 它們主要是D的運行時或正在運行的代碼中的聲明導致程序中的錯誤的結果。

Throwable派生的另一個類是Exception 它用於問題不是您的程序中的錯誤,而是由於用戶輸入或環境而引起的問題的情況(例如,用戶提供的XML無效,或者您的程序嘗試打開的文件不存在)。 異常為函數提供了一種報告其輸入無效或由於其無法控制的問題而無法完成其任務的方式。 然后,程序可以選擇捕獲該Exception並嘗試從中恢復,也可以讓它冒泡到頂部並殺死該程序(盡管通常,捕獲它們並打印出更用戶友好的內容更人性化而不是帶有堆棧跟蹤的消息)。 Error不同, Exception 確實導致運行所有清理代碼。 因此,捕獲它們並繼續執行是完全安全的。

但是,程序對異常的響應以及是否可以做更多的工作,而不是向用戶報告錯誤發生並終止,這取決於異常發生的原因和程序在做什么(這是某些代碼子類為何的一部分)。 Exception -它提供了一種方法,不僅可以報告錯誤消息,而且還可以報告發生了什么問題,並且允許程序根據發生錯誤的事件的類型以編程方式對其做出響應,而不僅僅是對“某事”發生錯誤的事實做出響應)。 通過使用異常來報告錯誤發生的時間,它可以使代碼不直接處理錯誤,除非它是您要在代碼中處理錯誤的位置,從而使代碼總體上更加簡潔,但不利的是有時您可以獲取異常被拋出,這是您不期望的,如果您不熟悉什么時候會被拋出。 但這也意味着報告的錯誤不會像錯誤代碼那樣被遺漏。 如果您忘記處理異常,那么您會在發生異常時知道它,而使用錯誤代碼之類的東西,很容易忘記檢查它或沒有意識到自己需要這樣做,並且錯誤可能會丟失。 因此,盡管意外的異常可能很煩人,但它們有助於確保您在程序中及時發現問題。

現在,使用斷言和異常的最佳時間可能有點技巧。 例如,在按合同設計時,您可以使用斷言來檢查函數的輸入,因為任何使用無效參數調用該函數的代碼都違反了合同,因此被認為是錯誤的,而在防御性編程中,您無需假設該輸入有效,因此該函數將始終檢查其輸入(不只是在不使用-release進行編譯時),並且在失敗時將引發Exception 哪種方法更有意義取決於您正在做什么以及該功能的輸入可能來自何處。 但是,使用斷言來檢查用戶輸入或程序控制范圍之外的任何內容都是不適當的,因為錯誤的輸入不是程序中的錯誤。

但是,雖然通常來說,在D中處理錯誤情況的慣用方式可能是引發異常,但有時確實沒有任何意義。 例如,如果錯誤情況實際上極有可能發生,則拋出異常是處理它的代價非常高的方法。 通常,異常情況對於並非一直發生的情況足夠快,但是對於經常發生的情況(尤其是對性能至關重要的代碼),異常情況可能會太昂貴。 在這種情況下,執行類似錯誤代碼的操作可能更有意義-或執行類似的操作(當函數無法獲得結果時,返回Nullable並將其設置為null)。

通常,當合理地假設該函數將成功執行和/或簡化代碼以使其成功使用時,使異常不必擔心錯誤情況時,異常才是最有意義的。

例如,假設編寫一個使用錯誤代碼而不是異常的XML解析器。 其實現中的每個函數都必須檢查其調用的任何函數是否成功,然后返回其自身是否成功,這不僅容易出錯,而且這意味着在整個解析器中到處都有錯誤處理代碼。 另一方面,如果使用異常,則大多數解析器不必關心XML中的錯誤。 它可以拋出異常,而不是遇到無效XML的代碼而必須返回一個錯誤代碼(調用該函數的函數必須處理該錯誤代碼),並且調用鏈中的任何代碼實際上都是處理錯誤的好地方(可能是代碼)那么首先調用解析器的代碼是唯一必須處理該錯誤的代碼。 這樣,程序中唯一的錯誤處理代碼就是需要處理錯誤的代碼,而不是大多數程序。 這樣的代碼更加干凈。

異常真正清除代碼的另一個示例是類似std.file.isDir的函數。 它返回給定的文件名是否與目錄相對應,並在出現問題(例如文件不存在或用戶無權訪問)時引發FileException 為了使用錯誤代碼,您將不得不做類似的事情

int isDir(string filename, ref bool result);

這意味着您不能簡單地將其置於類似

if(file.isDir)
{
    ...
}

你會被丑陋的東西困住

bool result;
immutable error = file.isDir(result);
if(error != 0)
{
    ...
}
else if(result)
{
    ...
}

的確,在很多情況下,文件不存在的風險很高,這可能是使用錯誤代碼的一個參數,但是std.file.exists可以在調用isDir之前輕松檢查該條件,從而確保isDir失敗是不常見的情況-或如果所討論的代碼是以很可能存在文件的方式編寫的(例如,它是從dirEntries ),那么您就不必費心檢查文件是否存在。 無論哪種方式,其結果都比處理錯誤代碼更整潔且更不易出錯。

無論如何,最合適的解決方案取決於您的代碼在做什么,並且在某些情況下,異常確實沒有意義,但是通常,它們是處理不是程序中的錯誤或錯誤的慣用方式。諸如內存不足之類的災難性錯誤和Error通常是處理程序中遇到的錯誤或災難性錯誤的最佳方法。 歸根結底,要知道何時以及如何使用異常與其他技術相比,是有點技巧的,並且通常需要經驗才能使人對它有良好的感覺,這就是為什么有關何時使用異常,斷言和錯誤的問題的一部分。代碼不時彈出。

暫無
暫無

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

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