錯誤、異常與自定義異常

程序員對於異常(Exception)這個詞應該都不陌生,尤爲如今Exception基本上是OOP編程語言的標配。於我而言,這個詞既熟悉又陌生,熟悉是由於聽過了不少遍、彷佛也有大量使用;陌生是由於不多真正思考過到底什麼是異常,以及如何使用異常。本文記錄我對如何使用異常、自定義異常的一些見解,不必定正確,還請多多指教。
本文地址:http://www.javashuo.com/article/p-rtdqeewa-cz.htmlhtml

什麼是異常

異常是錯誤處理的一種手段:java

exception handling is an error-handling mechanismpython

上述定義中的error是廣義的error,任何代碼邏輯、操做系統、計算機硬件上的非預期的行爲都是error。並非Java語言中與Exception對立的Error(Java中,Error和Exception是有區別的,簡而言之,Error理論上不該該被捕獲處理,參見Differences between Exception and Error),也不是golang中與panic對立的error。linux

在編程語言中,對於error的分類,大體能夠分爲Syntax errors、Semantic errors、Logical errors,若是從error被發現的時機來看,又能夠分爲Compile time errors、Runtime errors。git

結合實際的編程語言,以及wiki上的描述:程序員

Exception handling is the process of responding to the occurrence, during computation, of exceptions – anomalous or exceptional conditions requiring special processing – often disrupting the normal flow of program execution.github

能夠看出,通常來講,Exception對應的是Runtime error,好比下面的代碼golang

FileReader f = new FileReader("exception.txt"); //Runtime Error

若是文件不存在,就會拋出異常,但只有當程序運行到這一行代碼的時候才知道文件是否存在。編程

須要注意的是,異常並非錯誤處理的惟一手段,另外一種廣爲使用的方式是error codeerror code是一種更爲古老的錯誤處理手段,下一章節將會就error code與exception的優劣介紹。設計模式

何時使用異常

下面用兩個例子來闡釋何時使用異常。

初探異常

第一個例子來自StackExchange When and how should I use exceptions? .
題主須要經過爬取一些網頁,如http://www.abevigoda.com/來判斷Abe Vigoda(教父扮演者)是否還在世。代碼以下:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

def parse_abe_status(s):
    '''Param s: a string of the form "Abe Vigoda is something" and returns the "something" part'''
    return s[13:]

簡而言之,就是下載網頁內容,提取全部包含"Abe Vigoda"的句子,解析第一個句子來判斷"Abe Vigoda"是否尚在人世。

上述的代碼可能會出現幾個問題:

  • download_page因爲各類緣由失敗,默認拋出IOError
  • 因爲url錯誤,或者網頁內容修改,hits可能爲空
  • 若是hits[0]再也不是"Abe Vigoda is something" 這種格式,那麼parse_abe_status返回的既不是alive,也不是dead,與預期(代碼註釋)不相符

首先,對於第一個問題,download_page可能拋出IOError,根據函數簽名,函數的調用者能夠預期該函數是須要讀取網頁,那麼拋出IOError是能夠接受的。

而對於第二個問題 -- hits可能爲空,題主有兩個解決方案。

使用error code

在這裏,就是return None

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    if not hits:
        return None

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

顯然,這裏是經過error code(None)來告訴調用者錯誤的發生,上一章節也提到 error code是除了Exception handling以外的另外一種普遍使用的error handling 手段。

那麼error code相比Exception有哪些優缺點呢?
首先是優勢:

  • 沒有引入新的概念,僅僅是普通的函數調用
  • 易於理解,不會打亂當前的執行流

相比Exception,其缺點包括:

  • 代碼時刻都須要檢查返回值,而調用者很容易遺漏某些檢查,這就可能隱藏、推遲更嚴重問題的暴露

  • 缺少錯誤發生的上下文信息
  • 有的時候一個函數根本沒有返回值(好比構造函數),這個時候就得依賴全局的error flag(errno)

好比Linux環境下,linux open返回-1來表示發生了錯誤,但具體是什麼緣由,就得額外去查看errno

回到上述代碼,從函數實現的功能來講,check_abe_is_alive應該是比get_abe_status更恰當、更有表達力的名字。對於這個函數的調用者,預期返回值應該是一個bool值,很難理解爲何要返回一個None。並且Python做爲動態類型語言放大了這個問題,調用極可能對返回值進行conditional execution,如if check_abe_is_alive(url):, 在這裏None也被當成是False來使用,出現嚴重邏輯錯誤。

返回None也體現了error code的缺點:延遲問題的暴露,且丟失了錯誤發生的上下文。好比一個函數應該返回一個Object,結果返回了一個None,那麼在使用這個返回值的某個屬性的時候纔會出trace,但使用這個返回值的地方可能與這個返回值建立的地方已經隔了十萬八千里。沒有讓真正的、原始的錯誤在發生的時候就馬上暴露,bug查起來也不方便。

拋出異常

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    if status not in ['alive', 'dead']:
        raise SomeTypeOfError("Status is an unexpected value.")

    # he's either alive or dead
    return status == "alive"

注意上面的代碼同時也包含了第三個問題的解決方案,即確保statusalive或者dead兩者之一。不過咱們重點關注對hits爲空的處理。有兩點值得注意:

  1. 拋出的是自定義異常NotFoundError,而不是IndexError。這是一個明智的選擇,由於hits爲空是一個實現細節,調用者很難想象爲啥要拋出IndexError。關於自定義異常,後面還有專門的章節討論。
  2. 經過嘗試捕獲IndexError來判斷hits爲空,這個是不太推薦的作法,由於這裏明顯能夠經過if not hits來判斷hits是否爲空

關於用條件判斷(if) 仍是 try-catch, 在Best practices for exceptions中是這樣描述的

Use exception handling if the event doesn't occur very often, that is, if the event is truly exceptional and indicates an error (such as an unexpected end-of-file). When you use exception handling, less code is executed in normal conditions.

Check for error conditions in code if the event happens routinely and could be considered part of normal execution. When you check for common error conditions, less code is executed because you avoid exceptions.

if 仍是 try-catch,其實暗示了關於異常自己一個有爭議的點:那就是exception是否應該充當流程控制的手段,wiki上總結說不一樣的語言有不一樣的偏好。不過,我的認爲,若是能用if,就不要使用try-catch,exception僅僅使用在真正的異常狀況。

再探異常

第二個例子來自stackoverflow When to throw an exception ,題主的習慣是針對任何非預期的狀況都定義、拋出異常,如UserNameNotValidException, PasswordNotCorrectException, 但團隊成員不建議這樣作,所以題主發帖尋求關於異常使用的建議。

我想這是一個咱們均可能遇到的問題,捕獲並處理異常相對簡單,但何時咱們應該拋出異常呢,該拋出標準異常仍是自定義異常呢?咱們先看看StackOverflow上的回答

高票答案1:

My personal guideline is: an exception is thrown when a fundamental assumption of the current code block is found to be false.
答主舉了一個Java代碼的例子:判斷一個類是否是List<>的子類,那麼理論上不該該拋出異常,而是返回Bool值。可是這個函數是有假設的,那就是輸入應該是一個類,若是輸入是null,那麼就違背了假設,就應該拋出異常。

高票答案2:

Because they're things that will happen normally. Exceptions are not control flow mechanisms. Users often get passwords wrong, it's not an exceptional case. Exceptions should be a truly rare thing, UserHasDiedAtKeyboard type situations.
答主直接回答題主的問題,強調異常應該是在極少數(預期以外)狀況下發生的錯誤才應該使用,異常不該該是流程控制的手段

高票答案3:

My little guidelines are heavily influenced by the great book "Code complete":

  • Use exceptions to notify about things that should not be ignored.
  • Don't use exceptions if the error can be handled locally
  • Make sure the exceptions are at the same level of abstraction as the rest of your routine.
  • Exceptions should be reserved for what's truly exceptional.
    答主參考《代碼大全》認爲僅僅在出現了當前層次的代碼沒法處理、也不能忽略的錯誤時,就應該拋出異常。並且異常應該僅僅用於真正的異常狀況。

高票答案4:

One rule of thumb is to use exceptions in the case of something you couldn't normally predict. Examples are database connectivity, missing file on disk, etc.
異常應該僅僅因爲意料以外、不可控的狀況,如數據鏈接,磁盤文件讀取失敗的狀況

高票答案5:

Herb Sutter in his book with Andrei Alexandrescu, C++ Coding Standards: throw an exception if, and only if

  • a precondition is not met (which typically makes one of the following impossible) or
  • the alternative would fail to meet a post-condition or
  • the alternative would fail to maintain an invariant.

從上述回答能夠看出,若是違背了程序(routine)的基本假設(assumption、prediction、setup、pre-condition)h或者約束(post-condition、invariant),且當前層次的代碼沒法恰當處理的時候就應該拋出異常。

現代軟件的開發模式,好比分層、module、component、third party library使得有更多的地方須要使用異常,由於被調用者沒有足夠的信息來判斷應該如何處理異常狀況。好比一個網絡連接庫,若是鏈接不上目標地址,其應對策略取決於庫的使用者,是重試仍是換一個url。對於庫函數,拋出異常就是最好的選擇。

自定義異常

在上一章節中咱們已經看到了自定義異常(NotFoundError)的例子.

程序員應該首先熟悉編程語言提供的標準異常類,須要的時候儘可能選擇最合適的標準異常類。若是標準異常類不能恰如其分的表達異常的緣由時,就應該考慮自定義異常類,尤爲是對於獨立開發、使用的第三方庫。

自定義異常有如下優勢:

  • 類名暗示錯誤,可讀性強, 這也是標準庫、第三方庫也有不少異常類的緣由
  • 方便業務邏輯捕獲處理某些特定的異常
  • 可方便添加額外信息

    For example, the FileNotFoundException provides the FileName property.

Why user defined exception classes are preferred/important in java?中也有相似的描述

To add more specific Exception types so you don't need to rely on parsing the exception message which could change over time.
You can handle different Exceptions differently with different catch blocks.

通常來講,應該建立框架對應的特定異常類,框架裏面全部的異常類都應該從這個類繼承,好比pymongo

class PyMongoError(Exception):
    """Base class for all PyMongo exceptions."""


class ProtocolError(PyMongoError):
    """Raised for failures related to the wire protocol."""


class ConnectionFailure(PyMongoError):
    """Raised when a connection to the database cannot be made or is lost."""

異常使用建議

在知道何時使用異常以後,接下來討論如何使用好異常。

下面提到的實踐建議,力求與語言無關,內容參考了9 Best Practices to Handle Exceptions in JavaBest practices for exceptions

Exception應該包含兩個階段,這兩個階段都值得咱們注意:

  • Exception initialization:經過raise(throw)拋出一個異常對象,該對象包含了錯誤發生的上下文環境
  • Exception handling,經過try - catch(expect) 來處理異常,一般也會經過finally(ensure)來處理一下不管異常是否發生都會執行的邏輯,以達到異常安全,好比資源的釋放。

try-catch-finally
try-catch-finally代碼塊就像事務,不管是否有異常發生,finally語句都將程序維護在一種可持續,可預期的狀態,好比上面提到的資源釋放。不過爲了防止忘掉finally的調用,通常來講編程語言也會提供更友好的機制來達到這個目的。好比C++的RAII,python的with statement,Java的try-with-resource

若是能夠,儘可能避免使用異常
前面提到,exception應該用在真正的異常狀況,並且exception也會帶來流程的跳轉。所以,若是能夠,應該儘可能避免使用異常。``Specail case Pattern```就是這樣的一種設計模式,即建立一個類或者配置一個對象,用來處理特殊狀況,避免拋出異常或者檢查返回值,尤爲適合用來避免return null。

自定義異常, 應該有簡明扼要的文檔
前面也提到,對於第三方庫,最好先有一個於庫的意圖相匹配的異常基類,而後寫好文檔。

exception raise
對於拋出異常的函數,須要寫好文檔,說清楚在什麼樣的狀況下會拋出什麼樣的異常;並且要在異常類體系中選擇恰到好處的異常類,Prefer Specific Exceptions

clean code vs exception
《clean code》建議第三方庫的使用者對第三方庫可能拋出的異常進行封裝:一是由於對這些異常的處理手段通常是相同的;二是可讓業務邏輯於第三方庫解耦合。

In fact, wrapping third-party APIs is a best practice. When you wrap a third-party API, you minimize your dependencies upon it

exception handling
捕獲異常的時候,要從最具體的異常類開始捕獲,最後纔是最寬泛的異常類,好比python的Exception

In catch blocks, always order exceptions from the most derived to the least derived

程序員應該認真對待異常,在項目中看到過諸多這樣的python代碼:

try:
    # sth 
except Exception:
    pass

第一個問題是直接捕獲了最寬泛的類Exception;其次並無對異常作任何處理,掩耳盜鈴,固然,實際中也多是打印了一條誰也不會在意的log。

若是咱們調用了一個接口,而這個接口可能拋出異常,那麼應該用當前已有的知識去盡力處理這個異常,若是當前層次實在沒法處理,那麼也應該有某種機制來通知上一層的調用者。checked exception確定是比函數文檔更安全、合適的方法,不過諸多編程語言都沒有checked exception機制,並且《clean code》也不推薦使用checked exception,由於其違背了開放關閉原則,可是也沒有提出更好的辦法。

Wrap the Exception Without Consuming It
有的時候拋出自定義的異常可能會比標準異常更有表達力,好比讀取配置文件的時候 ConfigError(can not find config file)IoError更合適,又好比前面例子中的NotFoundError

不過,重要的是要保留原始的trace stack,而不是讓re-raise的stack。好比如下Java代碼:

public void wrapException(String input) throws MyBusinessException {
    try {
        // do something may raise NumberFormatException
    } catch (NumberFormatException e) {
        throw new MyBusinessException("A message that describes the error.", e);
    }
}

python2中是下面的寫法

def bar():
    try:
        foo()
    except ZeroDivisionError as e:
        # we wrap it to our self-defined exception
        import sys
        raise MyCustomException, MyCustomException(e), sys.exc_info()[2]

references

相關文章
相關標籤/搜索