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 結束,訊息指出是哪個函式的第幾個參數、收到什麼型別。
想一想:引數與型別
下面的程式會印出什麼?
<?php
function f($a, $b = 2, $c = 3) {
return "{$a}{$b}{$c}";
}
echo f(1, c: 9), ' ', f(4, 5);
下面的程式執行的結果是什麼?
<?php
declare(strict_types=1);
function double(int $n): int {
return $n * 2;
}
echo double("5");
可變參數、展開與傳參照
可變參數:參數前面加 ...,呼叫時給幾個引數都可以,全部收進一個陣列。它必須是最後一個參數,也可以加型別(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 也不影響它。
想一想:範圍與閉包
下面的程式會印出什麼?
<?php
$x = 10;
function f() {
$x = 5;
return $x;
}
echo f(), $x;
下面的程式會印出什麼?
<?php
function counter() {
static $n = 0;
$n++;
return $n;
}
echo counter() + counter() + counter();
下面的程式會印出什麼?
<?php
$n = 1;
$f = function () use ($n) {
return $n;
};
$n = 2;
$g = fn() => $n;
$n = 3;
echo $f(), $g(), $n;
遞迴與 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():
- 先打折:
$discount是折扣比例,0.2表示減去 20%(打八折) - 會員(
$member是 true)再減 50 元 - 結果不能低於 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等函式