National Taiwan University
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 67 24°C PM2.5 11
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 07

函式

定義函式、參數與回傳值的型別、預設值、具名引數、變數範圍、匿名函式與箭頭函式
Interaction實作 30 minute(s)

Learning Objectives

  • 定義有參數、預設值與回傳值的函式,並用位置引數或具名引數呼叫
  • 寫出參數與回傳值的型別宣告(含 ?int、int|string、void),說明 strict_types 開啟前後的差別
  • 使用可變參數 ...$nums 與展開 f(...$arr),分辨傳值與傳參照 &$x
  • 說明函式的變數範圍:函式裡看不到全域變數,以及 static 變數的用途
  • 寫出匿名函式(含 use 與 use (&$x))、箭頭函式 fn() =>,把函式當成引數傳給 array_map 等函式,並寫出簡單的遞迴

把一段程式包起來重複使用

同一段計算如果在程式裡出現好幾次,修改時就得改好幾個地方,很容易漏掉。 函式(function)把一段程式包起來、取一個名字,需要時用名字呼叫它:

function area(float $w, float $h): float {   // 定義:名稱、參數、回傳值的型別
    return $w * $h;                          // return 把結果交回呼叫的地方
}

echo area(3, 4.5);                           // 呼叫:3 和 4.5 是引數
  • 參數(parameter)是定義時括號裡的變數 $w、$h;引數(argument)是呼叫時實際給的值 3、4.5
  • return 把結果交回去,同時結束函式;沒有 return 的函式回傳 null
  • PHP 允許先呼叫、後定義:函式在程式開始執行前就已經準備好了

逐步執行動畫裡,呼叫函式時左邊會多出一個框架(frame),上面是函式名稱和它自己的變數;函式 return 之後,框架就消失。 PHP 本身也內建上千個函式,例如前幾課用過的 strlen、round、intdiv。

參數、預設值與具名引數

greet 有三個參數,後兩個有預設值,呼叫時可以省略。第 19、20 行用具名引數(named arguments,PHP 8.0 起):寫出參數名稱,就能跳過中間的參數,也不用照順序。注意每次呼叫時新框架裡的參數值。

型別宣告

參數名稱前面、右括號後面的 : 型別 是型別宣告,說明這個函式要收什麼、會回傳什麼。 寫上型別,傳錯資料時 PHP 會立刻告訴你,而不是算出奇怪的結果。

寫法 意思
int、float、string、bool、array 指定的型別
?int int 或 null(nullable,PHP 7.1 起)
int\|string int 或 string(聯合型別 union types,PHP 8.0 起)
mixed 任何型別(PHP 8.0 起)
: void 不回傳任何值(PHP 7.1 起),只能寫 return; 或不寫 return
callable 可以呼叫的東西:函式名稱字串、匿名函式等

沒有開啟 strict_types 時(預設),PHP 會盡量把引數轉成宣告的型別:字串 "5" 傳給 int 參數會變成整數 5, 整數 3 傳給 float 參數會變成 3.0;轉不過去(例如 "五")才丟出 TypeError。

在檔案第一個敘述寫 declare(strict_types=1); 開啟嚴格模式:型別必須完全相符,"5" 傳給 int 參數也會丟出 TypeError。 唯一的例外是 int 可以傳給 float。嚴格模式只影響這個檔案裡發出的呼叫。

弱型別模式下的自動轉換與 TypeError

這個檔案沒有 declare(strict_types=1)。看每次呼叫時框架裡參數的值:字串 "5" 進到函式裡已經變成整數 5。最後一行刻意傳入轉不過去的 "五",程式以 Fatal error 結束,訊息指出是哪個函式的第幾個參數、收到什麼型別。

想一想:引數與型別

1.

下面的程式會印出什麼?

<?php
function f($a, $b = 2, $c = 3) {
    return "{$a}{$b}{$c}";
}
echo f(1, c: 9), ' ', f(4, 5);
2.

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

<?php
declare(strict_types=1);
function double(int $n): int {
    return $n * 2;
}
echo double("5");
第 1 次作答

可變參數、展開與傳參照

可變參數:參數前面加 ...,呼叫時給幾個引數都可以,全部收進一個陣列。它必須是最後一個參數,也可以加型別(int ...$qtys)。

function sum(...$nums) { … }     // sum(1, 2, 3):$nums 是 [1, 2, 3];sum():$nums 是 []
sum(...$list);                    // 展開:把陣列的元素拆開當成一個一個的引數

傳值與傳參照:PHP 預設傳值,函式拿到的是引數的複本,在函式裡改參數不影響外面的變數(陣列也一樣,第 08 課會再看到)。 參數前面加 & 變成傳參照(pass by reference):函式裡的參數就是外面那個變數本身,改它等於改外面的變數。 內建的 sort($a) 就是這樣直接修改你給的陣列。傳參照時引數必須是變數,不能寫 addBonusRef(80)。

...$nums、展開與 &$score

sum(1, 2, 3) 呼叫時,看右邊:三個引數被收進 $nums 這個陣列。後半段比較兩個函式:addBonusCopy 改的是複本,addBonusRef 的參數有 &,改的是外面的 $s。注意兩次呼叫 return 時,框架裡的 $score 都是 85,但回到全域後 $s 的值不同。

變數範圍、static 與閉包

PHP 的函式看不到外面的變數:函式裡的 $rate 和全域的 $rate 是兩個不同的變數。 這和 JavaScript、Python 不同(它們的函式可以直接讀外層變數),從那兩種語言轉過來的人常常在這裡出錯。 函式需要什麼資料,最清楚的做法是當成參數傳進去。其他方式:

寫法 作用 建議
global $rate; 在函式裡使用全域的 $rate 不建議:看函式的參數看不出它依賴什麼,任何地方都能改
static $n = 0; 變數在呼叫結束後保留值,下次呼叫接著用;初始值只設定一次 計數器、快取
function () use ($x) { … } 匿名函式(閉包 closure),建立時把 $x 複製一份帶進去 常用
function () use (&$x) { … } 帶進去的是參照,函式裡改 $x,外面也會變 需要時
fn($p) => $p * $x 箭頭函式(PHP 7.4 起),自動捕捉用到的外層變數的值;只能寫一個運算式 簡短的回呼

匿名函式沒有名字,可以存進變數、用 $tax(100) 呼叫,也可以直接當成引數傳給別的函式。 use ($rate) 帶進去的是建立當下的值,之後外面的 $rate 改變,閉包裡的值不會跟著變;箭頭函式也一樣是在建立時捕捉值。

函式看不到外面的變數;use、use (&) 與 fn

第 5 行的 $rate 是 taxWrong 自己的變數,沒有設定過,所以出現 Warning、當成 null 計算。nextTicket 連續呼叫三次,看 static $n 怎麼接著上一次的值。第 20 行建立閉包時用 use 複製了當下的 $rate(0.05),第 23 行再改成 0.1 也不影響它。

想一想:範圍與閉包

1.

下面的程式會印出什麼?

<?php
$x = 10;
function f() {
    $x = 5;
    return $x;
}
echo f(), $x;
2.

下面的程式會印出什麼?

<?php
function counter() {
    static $n = 0;
    $n++;
    return $n;
}
echo counter() + counter() + counter();
3.

下面的程式會印出什麼?

<?php
$n = 1;
$f = function () use ($n) {
    return $n;
};
$n = 2;
$g = fn() => $n;
$n = 3;
echo $f(), $g(), $n;
第 1 次作答

遞迴與 callable

遞迴(recursion)是函式呼叫自己。把問題拆成「同樣的問題、但小一點」,再加上一個不再呼叫自己的終止條件:

function factorial(int $n): int {
    if ($n <= 1) return 1;            // 終止條件
    return $n * factorial($n - 1);    // 4! = 4 × 3!
}

忘了終止條件,函式會一直呼叫自己,最後耗盡記憶體。費氏數列的遞迴寫法很簡潔,但同樣的值會被重複計算很多次, n 稍微大一點就非常慢,動畫裡只算 fib(4)。

callable 是「可以呼叫的東西」:內建或自訂函式的名稱字串('strtoupper')、匿名函式、箭頭函式。 很多內建函式收 callable 當引數,例如 array_map($callback, $array) 對每個元素呼叫一次 $callback(第 08 課會介紹更多)。 function_exists('名稱') 檢查函式是否存在,is_callable($x) 檢查某個值能不能被呼叫。

遞迴的呼叫堆疊,與把函式當成引數

呼叫 factorial(4) 時,左邊的框架會一層一層疊上去:4、3、2、1。到了 $n <= 1 的終止條件才開始 return,框架再一層一層消失,每一層把結果乘上自己的 $n。後半段把箭頭函式、函式名稱字串傳給 applyTwice 和 array_map。

動手做:計算售價

完成函式 price():

  1. 先打折:$discount 是折扣比例,0.2 表示減去 20%(打八折)
  2. 會員($member 是 true)再減 50 元
  3. 結果不能低於 0,最後用 round() 四捨五入並轉成整數回傳

再補上最後一行:用具名引數呼叫 price(500) 並只指定會員,不指定折扣。

你的輸出
預期輸出

重點回顧

  • function 名稱(參數): 回傳型別 { … return 值; };沒有 return 的函式回傳 null;可以先呼叫後定義
  • 參數可以有預設值;具名引數 f(1, c: 9)(PHP 8.0)可以跳過有預設值的參數,具名引數要寫在位置引數後面
  • 型別宣告:int、?int、int|string、void、mixed、callable…;預設會自動轉換("5" 變成 5), declare(strict_types=1); 之後型別要完全相符,否則丟出 TypeError
  • ...$nums 把多個引數收成陣列,f(...$arr) 把陣列展開成引數
  • 預設傳值(函式拿到複本);參數加 & 是傳參照,可以修改外面的變數
  • 函式看不到全域變數:需要的資料當成參數傳入;global 不建議使用;static 變數在呼叫之間保留值
  • 匿名函式用 use ($x) 複製外層變數、use (&$x) 共用外層變數;箭頭函式 fn() =>(PHP 7.4)自動捕捉外層變數的值
  • 遞迴一定要有終止條件;函式名稱字串與閉包都是 callable,可以傳給 array_map 等函式
這一課完成了!
Next lesson:陣列
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。