1. 程式人生 > >有關PHP異常和錯誤處理機制的思考(一)

有關PHP異常和錯誤處理機制的思考(一)

我們從一門語言的層面上來看的話,這個語言通常具有很多的錯誤處理的一個模式,但是這些個錯誤處理模式,往往就是建立在約定俗成的基礎上,也可以說,這些錯誤都是可以預知的。

但是在大型的一個專案或者說系統裡,如果我們每次呼叫一個功能模組的時候,都去逐一檢測我們這個模組中肯能存在的錯誤,很明顯的就會看到,程式碼會變得冗餘和複雜,到處是try...catch,或者if...else等等判斷機制,並且還會很嚴重的影響程式碼的可讀性。

而且我們要知道,人為的因素是不可靠的,寫程式的人可能並不會把這些問題當成一回事,但是最後,可能就會導致系統故障。

基於上述情況,這些年逐漸形成了異常和錯誤處理機制,強迫消除這些問題,把問題提交給能解決它的環境,同時,把“描述正常過程中做什麼事情的程式碼”和“出了問題怎麼辦的程式碼”進行了一個分離。

這個異常的這種思想,可以追溯到20世紀60年代,在C++、Java中,發揚光大,而我們的PHP呢,就很明智的借鑑了這兩種語言的異常處理機制。

在PHP中,異常是指程式執行中,出現了不符合預期以及與正常的流程不同的情況。一種不正常的情況就是指按著正常邏輯不會出錯,但是嘞,它偏偏就是出現了錯誤,這可以看做是邏輯和業務流程的一種中斷,而不是語法錯誤。

而在PHP中,錯誤就是屬於自身的情況,是一種非語法或是環境導致的,讓編譯器無法通過檢查,甚至無法執行的情況。

在現有的各種語言裡,異常(exception)和錯誤(error)的概念是不一樣的,你在PHP裡,遇到任何一個自身錯誤都會觸發一個錯誤,而不是丟擲異常(還有一些情況會同時丟擲異常和錯誤),PHP一旦遇到非正常程式碼,都會觸發錯誤,而不是丟擲異常,在這個意義上,如果我們想使用異常來處理不可預料的錯誤,是辦不到的。例舉一個說法哈,比我我們想在檔案不存在且資料庫連線打不開的情況下觸發異常,這是不可能的,因為PHP會把它當成一個錯誤丟擲,而不是作為一個異常來自動捕獲。

來看一個經典的除零問題的示例程式碼感受下:

$a = null;
try {
    $a = 5/0;
    echo $a.PHP_EOL;
} catch (exception $e) {
    $e->getMessage();
    $a = -1;
}

echo $a;

上述程式碼執行的話,會出現兩種結果,一,空白頁面,二,warning警告(也是一種錯誤,只不過級別較低),由此我們可以看到,PHP裡,對於這種“除以零”這種異常情況,它會認為是一個錯誤,而不會認為是一個異常,更不會使得程式進入異常處理流程,所以,最終的結果值,也不會是我們預想中-1。在PHP裡,只有我們主動throw之後,我們才可以來捕獲異常(一般情況下是這樣的,但是有一些異常,PHP也是可以自動捕獲的)。

我們可以這樣認為,PHP無法自動捕獲有意義的異常,它會把所有不正常的情況都視作錯誤,我們如果想要捕獲這個異常,就必須得使用if...else這個結構,並且要保證程式碼是正常的,然後判斷我們的除數為0,則手動丟擲異常,然後再捕獲異常。

PHP內建的異常類主要有pdoexception、reflection exception,然後,我們來確定一個觀念,就是PHP只有我們自己手動丟擲異常之後,才可以嘗試捕獲異常,或者是我們有內建的異常機制時,PHP會先觸發錯誤,完事之後會捕獲異常。

來看下PHP裡異常的使用程式碼案例:

class emailException extends exception
{
    
    
}

class passwordException extends exception
{
    
    function __toString()
    {
       return "Exception ".$this->getCode()." :".$this->getMessage()." in File ".$this->getFile()." on line ".$this->getLine();
    }
}

function reg($reginfo=null)
{
    if(empty($reginfo) || !isset($reginfo)) {
        throw new Exception("undfined data");
    }

    if (empty($reginfo['email'])) {
        throw new emailException("the email content is empty");
    }

    if ($reginfo['pwd'] != $reginfo['repwd']) {
        throw new passwordException("twice password is not same");
    }

    echo "successfully";
}

try {
    reg(array('email' => "[email protected]", "pwd" => "201314", "repwd" => "2013141992"));
} catch (emailException $email) {
    echo $mail->getMessage();
} catch (passwordException $password) {
    echo $password;
    echo PHP_EOL."make";
} catch (Exception $e) {
    echo $e->getTraceAsString();
    echo PHP_EOL."other";
}

接下來,咱們就來分析下上述程式碼哈。

首先就是emailException和passwordException這兩個咱們自定義的異常類,一個只是繼承,另外一個則做出了咱們自己想要的異常資訊的一個輸出格式,我們可以根據自己的需求來定製自己需要的異常提醒模式,完事就可以根據不同的業務,來自己手動丟擲異常資訊,來快速確定錯誤位置。

完事就是下面這個reg檢測方法,它主要就是用來檢測傳入的一個數組,以及裡面的引數是否正確,如果沒有資料的話,就直接把異常發給exception這個超類,丟擲異常資訊,如果email的資訊不存在,則把異常發給emailException這個類,手動丟擲異常,結束程序,如果前後兩個密碼不正確的話,則把異常發給passwordException,手動丟擲異常,結束程序。

到這裡呢,我們就完成了異常的分發,但是還不夠,我們還需要對異常進行分揀,並且做出相應的處理,那就是最後一段程式碼的事情了。

我們可以自己手動嘗試變換不同的條件,來檢測我們最後的那個異常的分揀流程,不過要注意的是,exception作為超類,應該放在最後捕獲異常,不然的話,這個exception超類捕獲異常後,執行緒直接就停止了,後面的捕獲異常類,也就沒有用處了,還有就是這個exception超類,不能針對性的提示錯誤資訊和處理異常。

最後我們來看下經常會用到異常處理機制的幾個場景。

1、對程式的悲觀預測

如果一個程式設計師對自己的程式碼有“悲觀情緒”,咱不是說他的程式碼質量不高,而是說,他認為他的程式碼,不能很好的處理各種可預見的,不可預見的情況,那麼,這個人就會進行異常處理。

假設哈,程式設計師悲觀的認為自己的這段程式碼在高併發的情況下,會產生死鎖,那麼他就會悲觀的丟擲異常,然後在死鎖時,進行捕獲異常,之後,對該異常,進行細緻的處理。

2、程式的需要和對業務的關注

如果程式設計師不希望業務程式碼中充斥著大量的列印、除錯等處理,通常會使用異常處理機制,或者在業務上定義一些自己的異常,這個時候呢,就需要我們自定義一個異常,完事要把異常收集起來,到月底集中進行處理,還有就是希望我們的程式可以預見性的處理一些可能發生的或者會影響我們的正常業務程式碼。

在上述情況下,異常是業務處理中,必不可少的一個環節,還有就是,異常認為,資料一致很重要,我們在資料一致性可能被破壞的時候,就需要我們的異常機制進行事後的補救工作。

假如,我們要做個上傳檔案的功能,需要把檔案儲存在一個目錄裡,完事在資料庫中插入這個檔案的記錄,那麼,這兩步操作就可以看作是密不可分的整合業務,如果檔案上傳失敗,但是記錄儲存成功,這就會導致最後別人無法下載這個檔案。

那麼,我們可以這樣來思考,如果我們對檔案上傳成功或者資料庫插入成功不做提示,但是失敗的話,就丟擲異常的話,那麼我們就可以把這個上傳和插入的程式碼,放入我們的try...catch的程式碼塊裡,然後用catch模組來捕獲異常,完事可以對失敗的步驟進行處理,以此,就可以來保證資料的一致性了。

因此呢,從業務的層面來看異常的話,它主要就是保持業務資料的一致性,並且主要是對異常事務的一個違規處理。來看一個合理的try...catch程式碼模組:

還可以是如下格式:

上述的兩種捕獲異常的方式呢,第一種是在異常發生後,立即捕獲,最後一種就是分散拋異常,然後集中捕獲。至於我們選擇使用哪一種呢,就要看自己實際的需求了。

如果業務很重要,那麼自然是越早處理異常越好,可以保證程式在意外的情況下,可以持續處理業務,保證資料的一致性,舉個例子啊,比如一個操作,有多個前提步驟,前面走得好好的,最後一個突然就異常了,那麼其它的正常操作在這時候就需要全部移除掉了,這樣才能保證資料的一致性,並且在這種核心的業務下,有大量的程式碼來做善後的一個工作,進行資料補救,這是一種比較悲觀而又非常重要的異常,我們應該把它消滅在區域性,避免異常的擴散。

但是如果異常不是那麼重要,並且在單一入口、MVC風格的應用中,為了保持程式碼風格的統一,我們常常會採用最後一種異常處理的一個方式,這種方式更多的是強調一個業務流程的走向,而對善後的工作,則不是那麼關心,這種異常通常是次要異常,我們將其集中處理,主要是為了使得流程更加專一。

異常處理機制實際上可以把每一件事情當做事務來考慮,我們還可以把一場看作是內建的一個恢復機制,就是說如果某部分程式碼執行失敗,異常將恢復到某個已知的穩定的點上,這個點就是程式的上下文環境,而try塊裡的程式碼就儲存catch所要知道的程式上下文的資訊,因此如果很看重異常,就應當使用第一種異常處理和捕獲的方式。

3、語言級別的健壯性要求

提到這個健壯性啊,我們知道,PHP跟別的語言相比起來,是有點弱,例如、、、、、Java,提到這個我就上火啊,但是咱這是技術性文章,不應該帶有個人的想法,咱就直接說下這個java強在哪裡吧。

Java,支援多執行緒,Java認為多執行緒被中斷的情況,是無法預料和避免的,所以,Java使用者必須要正是這種情況,要麼你丟擲異常,不管它,要麼你就捕獲異常進行處理,反正不管怎樣,你必須意識到,異常可能發生,這類異常是強制性的,還有很多非強制性的,這個就看程式設計師自己了,總之java的對異常的分類和約束,保證了程式的健壯性。

我們很清楚,異常就是無法控制的程式執行時的錯誤,會中斷正常的邏輯執行,並且,這個異常後面的程式碼,也是不能執行,那麼在這裡這個try...catch程式碼塊的好處就顯而易見了。

它可以把異常造成的邏輯中斷的破壞性,降低到最小的範圍,還可以經過補救處理後,不影響業務邏輯的完整性,完事,亂拋異常,只拋異常,不捕獲,或者捕獲而不補救,都會導致資料混亂,這就是一場的一個重要的作用,它可以精確地控制執行時的流程,在程式中斷的時候,有預見的用try來縮小可能出錯的影響範圍,及時的捕獲異常,並且做出相應的處理,使得邏輯可以回到正常的軌道上。

我們明知道PHP異常機制是不足的,那我們有沒有什麼辦法可以很好地處理呢???

答案就是,結合PHP的錯誤處理機制來主動丟擲異常。

我們使用異常能在一定程度上降低程式的耦合性,但千萬不能濫用,濫用的後果,就是程式碼多處被掛起,流程變得更加複雜,並且難以理解,但是可以肯定的是,異常處理機制,在PHP中,有著很大的價值,越複雜的應用,我們越要考慮合理的來使用異常處理機制。

順道提一嘴,sql中也定義了一大堆exception,並且這些exception之間還存在著層級關係,不過,它們只是一個空殼,沒有什麼方法,需要我們自己手動填充使用 ,實際上,就是起到一個命名參考的作用。

好啦,本次記錄就到這裡了,關於錯誤處理,咱們下篇文章再繼續。

如果感覺不錯的話,請多多點贊支援哦。。。