前言
在程式設計中,函式是基本的程式碼重用機制。除了直接使用函式外,函式也是撰寫物件的基礎。本文說明如何在 PHP 中撰寫函式。
撰寫第一個函式
函式是一段可重覆使用的程式碼區塊。包含四個要件:
- 名稱 (識別字)
- 參數
- 回傳值
- 區塊
將 PHP 函式寫成虛擬碼如下:
<?php
# Pseudocode of a PHP function.
function funcName(argument)
{
# Implement this function here.
# Return some value in the middle
# or end of a function.
return value;
}
撰寫函式的保留字是 function
。此虛擬碼的函式名稱是 funcName
。參數可以零至多個,這裡的參數為 argument
。由於此函式是虛擬碼,沒有實作的部分。最後要回傳值時使用保留字 return
。由於 value
未預先宣告,實際上此虛擬碼無法執行。
從函式宣告中,無法看出回傳值是什麼,必需要實際追蹤函式的實作才行。PHP 可用型態宣告 (type declaration) 來補足這項缺失,詳見後文。
只看虛擬碼的話,會覺得比較抽象,缺乏真實感。在以下實例中,我們撰寫並呼叫了兩個函式:
<?php
# Some tests.
isEqual(power(3.0, 0), 1.0, 0.000001)
or die("It should be 1");
isEqual(power(3.0, 4), 81, 0.000001)
or die("It should be 81");
isEqual(power(3.0, -4), 1.0/81, 0.000001)
or die("It should be 1/81");
# Calculate the power of $base.
function power($base, $expo)
{
# Check whether a parameter is valid.
is_int($expo) or die("Invalid expo");
if (0 == $expo)
return 1.0;
# $result is merely an ordinary
# variable to represent a returning
# value.
$result = 1.0;
if ($expo > 0) {
for ($i = 0; $i < $expo; ++$i)
$result *= $base;
}
else {
for ($i = 0; $i > $expo; --$i)
$result /= $base;
}
return $result;
}
# Check whether two variables are equal.
function isEqual($a, $b, $delta)
{
return abs($a - $b) <= $delta;
}
第一個函式 power
是計算指數的函式。該函式接收兩個參數 $base
和 $expo
。回傳的是指數值。當 $expo
為 0
時,直接回傳 1.0
,不需計算。當 $expo
非零時,根據其值採用不同的計算方式,算完後回傳 $result
。
由於 PHP 已經內建 pow 函式,這裡的 power
函式僅止於練習,不要用在正式上線的程式碼中。
第二個函式 isEqual
用來檢查兩浮點數間是否相等。由於浮點數在計算時會產生微小誤差值,無法直接用 ==
(相等) 來檢查計算後的浮點數值。這裡使用比較簡單的計算方式。實際上處理浮點數誤差的方式會更加複雜,這已經超出本文的範圍。
在範例程式的尾端可以看到如何呼叫函式。寫好函式後,呼叫這些函式的方式和呼叫內建函式雷同。
在 PHP 程式中,使用函式的指令可以寫在宣告函式的代碼前,而且不需要預寫函式原型。本範例採用了這種寫法。這樣相當於先寫主程式,再寫函式本體,有利於閱讀者追蹤程式碼。
命名函式的方式
命名函式的方式和命名變數的方式大抵上相同。但函式不區分大小寫。即使 PHP 內建函式以 C 風格 (snake case) 來命名,實務上多以 Java 風格 (camel case) 來命名自訂函式。
參數的預設值 (Default Value)
PHP 函式可加上預設值。函式使用者就可以少寫幾個參數。以下函式使用一個預設值:
<?php
# Some text data.
$text = <<<EOL
PHP (recursive acronym for PHP: Hypertext Preprocessor)
is a widely-used open source general-purpose scripting
language that is especially suited for web development
and can be embedded into HTML.
Instead of lots of commands to output HTML (as seen in C
or Perl), PHP pages contain HTML with embedded code that
does "something" (in this case, output "Hi, I'm a PHP
script!"). The PHP code is enclosed in special start
and end processing instructions <?php and ?> that allow
you to jump into and out of "PHP mode."
EOL;
# Call a function with its default argument.
echo head($text);
# Simulate `head(1)` in PHP.
# A function with a default argument.
function head($text, $n = 10)
{
$list = explode(PHP_EOL, $text);
$result = [];
for ($i = 0; $i < count($list); ++$i) {
if ($i >= $n)
break;
array_push($result, $list[$i]);
}
return implode(PHP_EOL, $result);
}
本範例用 PHP 函式模擬 Unix head(1)
指令的行為。按照該 Unix 指令的慣例,不設置參數時顯示 10 行文字。這裡遵循此慣例,將該行為寫成函式預設值。
傳遞任意數量參數
除了使用固定數量的參數,PHP 函式也支援任意數量參數。以下範例使用了這項特性:
<?php
# Some test.
5 == maximal(2, 5, 4, 1, 3)
or die("Wrong number");
# Get maximal number from varargs.
function maximal(...$args)
{
$result = null;
# Iterate over varargs.
foreach ($args as $arg) {
# Check whether an argument is valid.
is_numeric($arg)
or die("Invalid argument: $arg");
# Set first argument as our base
# number.
if (is_null($result))
$result = $arg;
if ($arg > $result)
$result = $arg;
}
return $result;
}
範例函式 maximal
可接收一至多個參數,回傳這些參數的最大值。由於無法預先判斷參數的數量,要在程式中進行檢查。
不過,在 PHP 中,其實有更好的替代性做法。我們將前述範例改寫如下:
<?php
# Some test.
5 == maximal(2, 5, 4, 1, 3)
or die("Wrong number");
# Get maximal number from varargs.
function maximal()
{
$argc = func_num_args();
$argv = func_get_args();
$argc > 0 or die("No argument");
count($argv) == count(array_filter($argv,
fn($n) => is_numeric($n)
)) or die("Invalid argument(s)");
$result = $argv[0];
for ($i = 1; $i < $argc; ++$i) {
if ($argv[$i] > $result)
$result = $argv[$i];
}
return $result;
}
利用 PHP 內建函式 func_num_args 和 func_get_args 就可以取得參數的數量和值。之後再根據函式的需求來實作即可。
這行算是比較進階一點的寫法:
<?php
count($argv) == count(array_filter($argv,
fn($n) => is_numeric($n)
)) or die("Invalid argument(s)");
這行指令的目的在確認參數 $argv
是否為數字所組成的陣列。檢查的方式是利用 array_filter 過濾掉非數字的元素後,使用 count 分別計算出兩個陣列的的長度,再檢查兩者是否相等。使用 array_filter
時會以函式做為參數,這裡用到函數式程式設計的概念。
由於 PHP 已經提供 max 函式,這裡的 maximal
函式僅止於練習,不應用在生產環境的程式碼中。
宣告參數和回傳值的資料型態
雖然 PHP 是動態型態語言,但 PHP 函式可選擇性地加上資料型態宣告。在閱讀程式碼時可快速理解參數和回傳值的資料型態。以下範例用到此項特性:
<?php
# Some text data.
$text = <<<EOL
PHP (recursive acronym for PHP: Hypertext Preprocessor)
is a widely-used open source general-purpose scripting
language that is especially suited for web development
and can be embedded into HTML.
Instead of lots of commands to output HTML (as seen in C
or Perl), PHP pages contain HTML with embedded code that
does "something" (in this case, output "Hi, I'm a PHP
script!"). The PHP code is enclosed in special start
and end processing instructions <?php and ?> that allow
you to jump into and out of "PHP mode."
EOL;
# Call this function.
echo grep($text, "PHP");
# Simulate `grep(1)` in PHP.
# A function with type declarations.
function grep(string $source, string $target): string
{
$list = explode(PHP_EOL, $source);
$result = [];
foreach ($list as $line) {
if (false !== strpos($line, $target))
array_push($result, $line);
}
return implode(PHP_EOL, $result);
}
此範例用 PHP 函式模擬 Unix 指令 grep(1)
的行為。注意範例函式的參數和回傳值都加上了資料型態宣告。
PHP 官方網站提供了一份型態宣告的清單,有需要的讀者可以前往閱讀。
傳值 (Pass by Value) 和傳參考 (Pass by Reference)
在預設情境下,PHP 函式採用傳值呼叫。在傳值呼叫中,參數會拷貝一份,修改參數不會影響到外部變數。但 PHP 函式也提供傳參考呼叫的機制。在傳參考呼叫中,函式會直接修改外部變數。
若要使用傳參考呼叫,在參數前方加上 &
即可。以下範例用到此項特性:
<?php
# Some test.
$text = "Hello ";
append($text, "World");
"Hello World" == $text or die("Wrong text");
# Pass by reference.
function append(string &$dest, string $src): void
{
# $dest is modified inside a function.
$dest .= $src;
}
注意此範例的變數 $text
在函式呼叫後的值改變了。
使用傳參考呼叫時,其指令無法和傳值呼叫區別。所以函式實作者應謹慎使用此項特性,並在 API 文件中說明清楚。
回傳多個值
PHP 函式僅能回傳單一值。模擬回傳多個值的方式是回傳陣列 (或其他容器)。以下範例程式回傳一個陣列:
<?php
# Some test.
[$div, $mod] = divmod(10, 3);
3 == $div or die("Wrong division");
1 == $mod or die("Wrong modulus");
function divmod(int $a, int $b): int
{
# Check whether arguments are valid.
$a >= 0 or die("Wrong argument: $a");
$b >= 0 or die("Wrong argument: $b");
$div = 0;
while ($a > $b) {
++$div;
$a -= $b;
}
# Return an array to simulate
# multiple values.
return [$div, $a];
}
注意這一行指令:
<?php
[$div, $mod] = divmod(10, 3);
這裡在取得回傳值時同時進行解構賦值 (destructing assignment),省掉一個中繼陣列。
回傳陣列時,元素數量應以三個為限。回傳過長的陣列會造成記憶回傳值的困難,而且不利於日後重構程式碼。若要回傳多個回傳值,應改用關連式陣列。
遞迴函式 (Recursive Function)
遞迴函式是會呼叫自身的函式。這裡直接以實例來展示遞迴函式的撰寫和使用方式:
<?php
# Some test.
for ($i = 0; $i < 20; ++$i)
echo fib($i), "\n";
# A recursive function.
function fib(int $n): int
{
$n >= 0 or die("Wrong argument: $n");
if (0 == $n)
return 0;
elseif (1 == $n)
return 1;
# This function calls itself.
return fib($n - 1) + fib($n - 2);
}
使用遞迴函式和使用一般函式無異,因為遞迴函式是實作層面的議題。
此範例的函式用來計算 Fibonacci 數。其遞迴公式如下:
f(n) -> f(n - 1) + f(n - 2)
但這樣的公式會造成永無止境的循環。所以要在遞迴函式中設置終止條件:
f(0) -> 0
f(1) -> 1
f(n) -> f(n - 1) + f(n - 2)
了解遞迴函式的原理後,剩下的只是將其轉為 PHP 程式碼而已。
這個版本的範例函式每次都要從頭計算,無形中浪費了一些電腦效能。我們會在後續範例利用快取改善這項缺失。
保存函式的狀態
在預設情境下,PHP 函式是無狀態的,每次呼叫函式時都是從頭運算。這符合函式的慣例。但必要時,也可以儲存函式的狀態。當函式具有狀態時,函式的行為類似於物件。
儲存函式狀態的保留字為 static
,在想保存的變數前用此保留字來修飾即可。以下範例使用了此保留字:
<?php
for ($i = 0; $i < 20; ++$i)
echo fib(), "\n";
# A stateful function.
function fib(): int
{
# Keep the status of
# $a and $b.
static $a = 0;
static $b = 1;
$result = $a;
$c = $a + $b;
$a = $b;
$b = $c;
return $result;
}
在每次運算時,$a
和 $b
的狀態保留下來,所以每次回傳的值會根據這兩個變數的值而異。每次呼叫此函式時,相當於一次迭代,所以這裡不需要加上迴圈或遞迴。
這項機制也可用來改善遞迴程式的效能。像是以下的例子:
<?php
# Some test.
for ($i = 0; $i < 20; ++$i)
echo fib($i), "\n";
# A recursive function with a cache.
function fib(int $n): int
{
$n >= 0 or die("Wrong argument: $n");
static $cache = [];
if (array_key_exists($n, $cache))
return $cache[$n];
if (0 == $n) {
$cache[$n] = 0;
}
elseif (1 == $n) {
$cache[$n] = 1;
}
else {
$result = fib($n - 1) + fib($n - 2);
$cache[$n] = $result;
}
return $cache[$n];
}
此範例函式將運算過的結果儲存在 $cache
中,下次碰到相同的 $n
時,直接從 $cache
取出值即可,省掉了重覆的運算過程。這是使用空間 (記憶體) 換取時間 (CPU 運算) 的典型例子。