位元詩人 [PHP] 程式設計教學:函式 (Function)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

在程式設計中,函式是基本的程式碼重用機制。除了直接使用函式外,函式也是撰寫物件的基礎。本文說明如何在 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。回傳的是指數值。當 $expo0 時,直接回傳 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_argsfunc_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 運算) 的典型例子。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。