Minghsin University of Science and Technology
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try a Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

1

Local AI

--

Uedu Code

--

明新科大 AQI 70 24°C PM2.5 19
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 13

例外處理

try、catch、finally、throw,自訂例外,Error 與 Exception
Interaction實作 25 minute(s)

Learning Objectives

  • 分辨 Warning 與例外:Warning 印出訊息後繼續執行,沒有被接住的例外會讓程式停止
  • 用 try、catch、finally 處理例外,說明 finally 在 return 時也會執行
  • 用 throw 丟出適當的例外,看懂 Exception 與 Error 兩個家族與 Throwable 的關係
  • 用多重 catch 與 `catch (A | B $e)` 分別處理不同的例外,並注意 catch 的順序
  • 寫出繼承 Exception 的自訂例外,用 previous 串起例外鏈,並設定全域的錯誤與例外處理函式

事情不會總是照計畫進行

檔案可能不存在、使用者可能輸入「abc」當數量、資料庫可能連不上、餘額可能不夠付款。好的程式不是「不會出錯」, 而是出錯的時候知道該怎麼辦:給使用者看得懂的訊息、把細節寫進紀錄檔、確保資料不會只改了一半。

PHP 有兩種回報問題的方式:

錯誤訊息(Warning、Notice、Deprecated) 例外(exception)
長相 Warning: Undefined array key "Ken" in … on line 4 一個物件,例如 InvalidArgumentException
發生後 印出訊息,繼續執行下一行 立刻跳離目前的程式,往外找能處理它的 catch
沒人處理 只是一行訊息 Fatal error: Uncaught …,整個程式停止
能不能用 try/catch 接 預設不行 可以

例外的好處是不會被忽略:函式遇到它處理不了的狀況時丟出(throw)例外,呼叫它的程式必須處理,否則程式就停下來, 不會帶著錯誤的資料繼續跑。

Warning 與沒有被接住的例外

第 4 行讀取不存在的鍵,只是 Warning,第 5 行照樣執行。第 16 行傳入空陣列,average 用 throw 丟出例外,沒有任何 catch 接住,程式就在這裡停止,第 17 行不會執行。看看最後印出的 Stack trace(呼叫堆疊):它列出例外發生時,程式是經過哪些函式呼叫走到這裡的。

看懂未捕捉例外的訊息

Fatal error: Uncaught InvalidArgumentException: 至少要有一個分數 in /var/www/html/index.php:10
Stack trace:
#0 /var/www/html/index.php(16): average(Array)
#1 {main}
  thrown in /var/www/html/index.php on line 10
部分 意思
Uncaught InvalidArgumentException 沒有被接住的例外,和它的類別
至少要有一個分數 例外的訊息(throw 時給的字串)
index.php:10 例外在第 10 行被建立
#0 index.php(16): average(Array) 第 16 行呼叫了 average,傳入一個陣列
#1 {main} 最外層:主程式

閱讀時從上往下:最上面是出事的地方,往下是「誰呼叫了它」。正式網站不能把這段訊息顯示給使用者(會洩漏路徑與程式結構), 本課後面的 set_exception_handler 可以接手處理。

try、catch、finally

try {
    // 可能丟出例外的程式
} catch (InvalidArgumentException $e) {
    // try 裡丟出 InvalidArgumentException(或它的子類別)時執行;$e 是那個例外物件
} finally {
    // 不論有沒有例外、有沒有被接住,最後都會執行
}
  • try 裡某一行丟出例外,後面的行就不會執行,直接跳到符合的 catch
  • 沒有丟出例外時,catch 整段跳過
  • finally 用來做「一定要做」的收尾:關閉檔案、釋放鎖定、記錄結束時間。即使 try 裡有 return,finally 也會在函式真正回傳之前執行
  • catch 和 finally 至少要有一個;catch 可以有好幾個

例外物件常用的方法:

方法 回傳
getMessage() 訊息字串
getCode() 錯誤代碼(整數,建立時沒給就是 0)
getLine()、getFile() 例外建立的行號與檔案
getTraceAsString() 呼叫堆疊的文字
getPrevious() 前一個例外(例外鏈,後面介紹)

try/catch/finally 的執行順序

迴圈跑兩次:第一次 10 / 4 正常,第二次 5 / 0 在 divide 裡丟出例外。注意第二次的第 14 行沒有執行,直接從 divide 跳到第 15 行的 catch;兩次都會執行第 18 行的 finally。

想一想:執行順序

1.

下面的程式會印出什麼?

<?php
function f() {
    try {
        echo 'A';
        return 'B';
    } finally {
        echo 'C';
    }
}
echo f();
2.

下面的程式會印出什麼?

<?php
try {
    echo '1';
    throw new RuntimeException('x');
    echo '2';
} catch (RuntimeException $e) {
    echo '3';
} finally {
    echo '4';
}
echo '5';
第 1 次作答

例外的家族:Exception 與 Error

所有能被 throw 的東西都實作了 Throwable 介面(PHP 7 加入),底下分成兩大家族:

Throwable(介面,所有能被 throw 的東西)
├── Exception                       程式預期可能發生、應該處理的狀況
│   ├── ErrorException              由 Warning 等錯誤轉成的例外
│   ├── LogicException              程式寫錯了(呼叫的方式不對)
│   │   ├── InvalidArgumentException  引數不合理
│   │   ├── DomainException
│   │   ├── LengthException
│   │   └── OutOfRangeException
│   ├── RuntimeException            執行時才發現的問題(外部狀況)
│   │   ├── OutOfBoundsException
│   │   ├── RangeException
│   │   ├── OverflowException
│   │   └── UnexpectedValueException
│   └── JsonException 等其他內建例外
└── Error                           PHP 引擎發現的錯誤,多半是程式寫錯
    ├── TypeError                   型別不對
    │   └── ArgumentCountError      引數個數不對
    ├── ValueError                  型別對但值不合理
    ├── ArithmeticError
    │   └── DivisionByZeroError     除以 0、對 0 取餘數
    ├── CompileError
    │   └── ParseError              語法錯誤
    └── UnhandledMatchError         match 沒有對應的分支
  • Exception:你的程式主動丟出、預期呼叫者會處理的狀況。自己丟例外時,從這一支挑一個意思最接近的類別(或自訂,後面介紹)
  • Error:PHP 引擎在執行時發現的錯誤,例如型別不對、除以 0、呼叫不存在的函式。PHP 8.0 起,內建函式收到型別不對的引數會丟出 TypeError, PHP 8.0 也新增了 ValueError(型別對但值不合理,例如 str_repeat('ab', -1));在這之前,很多情況只是 Warning 加上回傳 null

catch (Exception $e) 接不到 Error 家族,兩者是平行的兩支。真的要「全部接住」時寫 catch (Throwable $e), 但通常只在最外層(記錄錯誤、顯示「系統忙碌中」)這樣做;在一般的程式裡接住 Error,常常只是把程式寫錯的地方藏起來。

catch 比對的是「是不是這個類別或它的子類別」:catch (LogicException $e) 也會接住 InvalidArgumentException。

多重 catch 的寫法:

try {
    …
} catch (TypeError | ValueError $e) {        // PHP 7.1 起:一個 catch 接住多種例外,用 | 分隔
    …
} catch (DivisionByZeroError $e) {
    …
} catch (Throwable $e) {                     // 範圍大的放最後
    …
}

PHP 由上往下找第一個符合的 catch,所以子類別要寫在前面。如果把 catch (Exception $e) 寫在 catch (RuntimeException $e) 前面, 後者永遠不會被執行到。

內建的 Error 與多重 catch

三個輸入 '10'、'abc'、'0' 分別走到不同的地方:正常、TypeError、DivisionByZeroError。最後一段把 catch (Exception $e) 寫在 catch (RuntimeException $e) 前面,看看哪一個接到例外。

自訂例外與例外鏈

內建的例外類別只說得出「哪一類問題」。業務上的狀況(餘額不足、庫存不夠、帳號被停用)可以自訂例外類別, 讓呼叫者用 catch 精準地分辨,還能附帶額外的資料。

class InsufficientFundsException extends Exception
{
    public int $shortage;                              // 自己加的屬性:還差多少錢

    public function __construct(int $shortage, ?Throwable $previous = null)
    {
        $this->shortage = $shortage;
        parent::__construct("餘額不足,還差 {$shortage} 元", 1001, $previous);
    }
}

這是本課程第一次寫 class,只要看懂三件事,其他的(屬性、方法、$this、建構子、繼承)第 14 課會完整介紹:

  1. class 名稱 extends Exception:新的類別繼承 Exception,所以它也是一種 Exception,getMessage() 這些方法都能用,catch (Exception $e) 也接得到
  2. __construct 是建立物件時(new InsufficientFundsException(300))自動執行的函式
  3. parent::__construct(訊息, 代碼, 前一個例外) 把訊息交給 Exception 原本的建構子保存

例外鏈:低層的程式丟出例外,高層的程式接住後,換成一個對自己的呼叫者更有意義的例外再丟出,同時把原本的例外當成第三個參數 previous 傳進去。 這樣上層看到的是「結帳失敗」,需要追查時再用 getPrevious() 找到真正的原因「餘額不足」,資訊不會遺失。

自訂例外與例外鏈

第一次結帳 300 元成功;第二次 800 元,withdraw 丟出自訂的 InsufficientFundsException,checkout 接住後包成 RuntimeException 再丟出去。看右邊的物件:最後 $e 和 $prev 是兩個例外物件。

全域的錯誤與例外處理

把 Warning 變成例外:Warning 不能用 try/catch 接住。set_error_handler() 可以指定一個函式,每次出現 Warning、Notice 時改由它處理; 在這個函式裡丟出 ErrorException,所有的 Warning 就都變成可以 catch 的例外,不會被默默忽略。很多框架預設就是這樣設定的。

set_error_handler(function (int $no, string $msg, string $file, int $line) {
    throw new ErrorException($msg, 0, $no, $file, $line);
});

最後一道防線:set_exception_handler() 指定一個函式,處理所有沒有被任何 catch 接住的例外。 它執行完之後程式就結束了(不會回到丟出例外的地方),適合用來把細節寫進紀錄檔,再顯示一個友善的錯誤頁面。

正式網站的設定:display_errors 關閉(不把錯誤訊息顯示給使用者)、log_errors 開啟(寫進紀錄檔);開發時則打開 display_errors 方便除錯。

錯誤處理函式與例外處理函式

第 8 行讀取不存在的檔案,原本只會印出 Warning;設定了錯誤處理函式之後,Warning 變成 ErrorException,被第 10 行的 catch 接住。第 20 行丟出的例外沒有 catch,交給第 14 行設定的例外處理函式。

想一想:例外的家族與鏈

1.

下面的程式執行的結果是什麼?

<?php
try {
    echo intdiv(10, 0);
} catch (Exception $e) {
    echo '接住了';
}
2.

下面的程式會印出什麼?

<?php
class AppException extends Exception {}
try {
    try {
        throw new ValueError('壞值', 7);
    } catch (ValueError $e) {
        throw new AppException('包起來', 1, $e);
    }
} catch (AppException $e) {
    echo $e->getCode(), ' ', $e->getPrevious()->getMessage();
}
3.

程式讀取不存在的陣列鍵 $a['x'],PHP 印出 Warning: Undefined array key "x"。用 try { … } catch (Throwable $e) { … } 包住這一行,會怎麼樣?

第 1 次作答

動手做:檢查訂購數量

完成 parseQty:輸入不是由數字組成時丟出 InvalidArgumentException('不是整數');數字不在 1 到 10 之間時丟出 RangeException('數量要在 1 到 10 之間')。主程式逐一處理五個輸入:成功印「輸入:OK」並加進合計,失敗印「輸入:錯誤訊息」,最後印出合計。

你的輸出
預期輸出

重點回顧

  • Warning 印出訊息後繼續執行;例外沒有被接住時印出 Fatal error: Uncaught … 與 Stack trace,程式停止
  • try 裡丟出例外後,剩下的行不執行,跳到第一個符合的 catch;finally 一定會執行,return 時也會在回傳前執行
  • throw new 類別(訊息, 代碼, previous) 丟出例外;常用方法 getMessage、getCode、getLine、getTraceAsString、getPrevious
  • Throwable 底下兩支:Exception(程式預期的狀況,InvalidArgumentException、RuntimeException…)與 Error(引擎發現的錯誤,TypeError、ValueError、DivisionByZeroError…);catch (Exception $e) 接不到 Error
  • catch (A | B $e) 一次接多種;catch 由上往下比對,子類別寫前面
  • 自訂例外:class X extends Exception,在建構子裡呼叫 parent::__construct(…);用 previous 串起例外鏈,上層用 getPrevious() 追查原因
  • set_error_handler 把 Warning 轉成 ErrorException;set_exception_handler 處理沒被接住的例外,執行完程式就結束
這一課完成了!
Next lesson:物件導向程式設計
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。