php中的異常機制,只能算是一個舶來品,php的書中對異常機制討論的很少,大多僅停留在語法階段。有人盛贊php的異常是個好東西,也有人認為php的異常很不給力,也有人一直困惑在php中該不該用異常,怎麼用?
異常本身的語法並不值得討論,異常的使用場景才是主要的,這裡我對比php和java,來看看php裡的異常到底是怎麼回事,異常到底應該怎麼用。
看到了PPC論壇上的這篇討論,覺得很有價值,我重新整理了下我的觀點,做個總結。
首先,需要說的是,這裡的異常是指php的異常。因為php的異常和其它語言相比有著很大的不同。
php裡的異常,是程序運行中的不符合預期的情況,即一種在程序執行流程裡面允許發生,只是和正常流程不同的狀況。它是一種不正常的情況,就是按照我們的正常邏輯本不該出錯,但仍然會出現的錯誤,屬於邏輯和業務流程的錯誤,而不是語法上的錯誤。
php裡的錯誤則是一種非法的,語法或者環境問題導致的讓編譯器無法通過檢查,甚至無法運行的情況。
php的異常處理所做的是對你程序運行時出現的某種情況進行處理,並不是錯誤,異常是程序運行得到的結果不是你想要的。對於程序而言,異常是不可控的,我們無法控制運行時在哪個環節會出錯,但是我們可以大致預期到哪些環節會出錯,並進行針對性的補救。
異常(exception)和錯誤(error)的概念以及區分在各種語言裡是不一樣的。在java和php裡,對錯誤和異常的界定也是不同的。在php裡,它遇到任何的自身錯誤都會觸發一個錯誤,而不是拋異常(對於一些情況,會同時拋出異常和錯誤)。php一旦遇到非正常的代碼,通常都會觸發錯誤,而不是拋出異常。在這個意義上,如果你想使用異常來處理不可預料的問題,是辦不到的。比如說,你想在文件不存在,數據庫連接打不開的時候觸發異常,是不可行的。這在php裡是一種錯誤,php把它作為錯誤拋出,而無法作為異常自動捕獲。而java則不同,java把很多行為看成是異常並且可捕獲。
我們來個最直觀最簡單的例子吧。就以經典的除零問題為例:
運行結果:
下面是java代碼:ExceptionTry.java
代碼如下 復制代碼 //ExceptionTry.java運行結果:
如果我們把tp方法中的第二條語句改為如下:
a=5/1;
那麼結果將是如下:
由以上運行結果可以看到,對於除0這種“異常”代碼,php認為這是一個錯誤,會直接觸發錯誤(waring也是錯誤,只是錯誤等級不一樣而已),而不會自動拋異常使進入異常流程,故最終$a的值並不是預想中的-1,也就是說,並沒有進入異常分支,也沒有處理異常。php只有你主動throw後,才能捕獲異常(一般情況是這樣的,也有一些異常php可以自動捕獲)。
在下面三種場景下會用到異常處理機制:
(1)對程序的悲觀預測
如果一個程序員對自己的代碼帶有“悲觀情緒”,這裡並不是指該程序員代碼質量不高。他認為自己的代碼無法一一處理各種可預見的不可預見的情況,那該程序員就會進行異常處理。假設一個場景,程序員悲觀地認為自己的這段代碼在高並發條件下可能產生死鎖,那麼他就會悲觀地拋出異常,然後在死鎖時進行捕獲,對異常進行細致的處理。
(2)程序的需要和對業務的關注
如果程序員希望業務代碼中不會充斥大堆的打印,調試等處理,通常他們會使用異常機制;或者業務上需要定義一些自己的異常,這個時候就需要自定義一個異常, 來對現實世界中各種各樣的業務進行補充。比如上班遲到,這種情況,我就認為是一個異常,要收集起來,到月底集中處理,扣你工資;如果程序員希望有預見性地處理可能發生的會影響正常業務的代碼,那麼它需要異常。在這裡,強調了異常是業務處理中必不可少的環節,不能對異常視而不見。異常機制認為,數據一致很重要,在數據一致性可能被破壞時,就需要異常機制來進行預先補救。
舉個例子,比如有個上傳文件的業務需求,要把上傳的文件保存在一個目錄裡,並在數據庫裡插入這個文件的記錄,那麼這兩步就是互相關聯密不可分的一個集成的業務,缺一不可。文件保存失敗,而插入記錄成功就會導致無法下載文件;而文件保存成功數據庫寫入失敗,則會導致沒有記錄的文件成為死文件,永遠得不到下載。
那麼我們假設文件保存成功後沒有提示,但是保存失敗會自動拋出異常,訪問數據庫也一樣,插入成功沒有提示,失敗則自動拋出異常,我們就可以把這兩個有可能拋出異常的代碼段包在一個try語句裡,然後在catch捕捉錯誤,在catch代碼段裡刪除沒有被記錄到數據庫的文件或者刪除沒有文件的記錄,以保證業務數據的一致性。 因此,從業務這個角度講,異常偏重於保護業務數據一致性,並且強調了對異常業務的處理。
如果我們的代碼中,只是象征性的try-catch,最後打印一個報錯,over。這樣的異常,不如不用,沒有體現了異常的思想。所以,合理的代碼應該如下:
也可以如下:
代碼如下 復制代碼 <?php上面的兩種捕獲異常的方式,前一種是在異常發生時,立刻捕獲;後一種是分散拋異常,集中捕獲。那到底應該是哪一種呢?
如果我們的業務很重要,那麼異常越早處理越好,以保證程序在意外情況下能保持業務處理的一致性。比如一個操作有多個前提步驟,突然最後一個步驟異常了,那麼其他前提操作都要消除掉才行,保證數據一致性。並且在這種核心業務下,有大量的代碼來做善後工作,進行數據補救,這是一種比較悲觀的異常。
如果我們的異常不是那麼重要,並且在單一入口,MVC風格的應用中,為了保持代碼流程的統一,則常常采用後一種異常處理方式,這種異常處理更多強調了業務流程的走向,對善後工作並不是很關心。這是一種樂觀的異常。
(3)語言級別的健壯性要求
在這點上,php是缺失的。以java為例,java是一種面向企業級開發的語言,強調健壯性。java中支持多線程,java認為,多線程被中斷這種情況是徹徹底底的無法預料和避免的。所以 java規定,凡是用了多線程,就必須正視這種情況。你要麼拋出,不管它,要麼捕獲,進行處理。總之,你必須面對 InterruptedException這個異常,不准回避。也就是異常發生後應對重要數據業務進行補救,當然你可以不做,但是我會告訴你,這是你應該做的。 這類異常是強制的。更多的異常是非強制的,由程序員決定的。java對異常的這種分類和約束,保證了java程序的健壯性和可信賴度。
那麼異常的意義何在?
異常就是無法控制的運行時錯誤,會導致出錯時中斷正常邏輯運行,該異常代碼後面的邏輯都不能繼續運行。那麼try/catch的好處就是可以把異常造成的邏輯中斷破壞降到最小范圍內,並且經過補救處理措施後不影響業務邏輯的完整性,亂拋異常和只拋不捕獲,或捕獲而不補救,會導致數據混亂。 這就是異常處理的一個重要作用,就是在我們精確控制運行時流程的時候,在程序中斷的時候,有預見的用try縮小可能出錯的影響范圍,再及時捕獲異常的發生並作出相應的補救,以使邏輯流程仍然能回到正常軌道上來。
怎樣看php的異常?
我們已經看到了php中的異常機制是很雞肋的,絕大多數情況下無法自動拋異常,必須用if-else來先進行判斷,再手工拋出異常。這種處理方式看起來,比較像是多此一舉。手動拋異常的意義就不是很大了,因為你手動拋異常也就意味著你在代碼裡已經充分預期到錯誤的出現了,也就算不得真正的“異常”了,而是意料之中的了。還是陷入了紛繁復雜的業務邏輯判斷和處理中。java和C++語言做的比較好的就是定義了一堆內置的常見的異常,不需要程序員判斷各種異常情況後手工拋出,編譯器會代我們進行判斷業務是否發生錯誤,自動拋出異常。作為程序員,則只需要關心異常的捕獲和隨後補救,而不是像php中關注到底會發生哪些異常啊,用if-else來逐一判斷,逐一拋異常。
php的異常機制很不完美,很多情況下和if-else相比沒有明顯的優勢,這也是php的異常沒有普及的原因。當然了,使用了異常也能一定程度上降低耦合性。
那怎麼來完善php原先的異常處理機制呢?這時,就要借助php的錯誤處理了。PHP提供了一個set_error_handler函數,可以自定義錯誤處理函數,能夠把非致命類型的錯誤處理都轉向到自己定義的函數裡進行分析和處理。但是因為出錯的地方可能很多,集中處理的話要區分的情況很復雜,所以我們只用這個特性做個跳板。在自定義函數裡我們手動拋一個異常出來,殺個回馬槍,讓try/catch可以捕獲並處理這個運行時錯誤所帶來的中斷,從而實現擴大try/catch影響范圍的目的.