深入探討 PHP 5.3 的 Closure 觀念與用法

在 PHP 4 最早稱 callback,PHP 5.3 引入 closureanonymous function ,PHP 5.4 則新增 callable type hint。

所以在 PHP 中,callbackclosureanonymous function,與 callable,事實上指的是同一件事情,但因為底層用的都是 Closure 物件,通常統稱為 closure。

Version


PHP 4 callback
PHP 5.3 closure / anonymous function
PHP 5.4 callable

Callback


將函式名稱以字串方式傳入,透過 call_user_func()call_user_func_array() 去執行該字串為名稱的函式,類似 C 語言以 function pointer 實現 callback。1 1本文強調 closure 基本語法,關於實務上運用,詳細請參考實務上如何活用 Closure?

最簡單的Callback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function myCallback() 
{
echo 'World';
}

function sayHello($callback)
{
echo 'Hello ';
call_user_func($callback);
}

sayHello('myCallback');

// Result:
// Hello World

第1行

1
2
3
4
function myCallback() 
{

echo 'World';
}

我們自己寫的 callback,只是一個普通的函式。

第6行

1
2
3
4
5
function sayHello($callback) 
{

echo 'Hello ';
call_user_func($callback);
}

我們傳入一個 callback 函式,2 2由於PHP是弱型別語言,只看到傳入$closure很難讓我們知道到底傳入的是普通變數,或者真的是一個 callback,所以在 PHP 5.4 才有了 callable type hint。希望在執行 echo 'Hello' 之後,能執行我們傳入的 $callback,這時會用到 PHP 內建的 call_user_func() 來呼叫我們的傳入的 callback 函式。

12行

1
sayHello('myCallback');

呼叫 sayHello(),並傳入 myCallback,注意這裏傳的是 callback 函式名稱的字串。3 3這個字串很容易讓我們誤解是普通字串,或者是真的是一個 callback 函式,所以在 PHP 5.3 讓我們可以直接傳入一個 anonymous function。

Anonymous Function


沒有名稱的函式,可被指定到變數,如同物件般傳遞到其他函式,通常使用在只使用一次的 callback,讓我們不必再為了只使用一次的 callback 定義函式。

最簡單的 Anonymous Function

1
2
3
4
5
6
7
8
$anonyFunc = function ($name) {
return 'Hello ' . $name;
};

echo $anonyFunc("Josh");

// Result:
// Hello Josh

第1行

1
2
3
$anonyFunc = function ($name) {
return 'Hello ' . $name;
};

建立了 Closure 物件,並且指定給 $anoy_func變數,它看起來像函式,卻是個物件,與函式相同語法,可接受參數,也可回傳參數,然而它卻沒有函式名稱。

Anonymous Function 內部實作
當我們使用 $anony_func 時,$anony_func 變數是一個 Closure 物件,它實作了 __invoke() magic method,當物件加上 () 後,PHP就會呼叫其 __invoke() magic method 執行 anonymous function。

以上的範例,也可以改用 __invoke() 方式執行。

1
2
3
4
5
6
7
8
$anonyFunc = function ($name) {
return return 'Hello ' . $name;
};

echo $anonyFunc->__invoke("Josh");

// Result:
// Hello Josh

將 Anonymous Function 用於 Callback
許多 PHP 內建的函式都需要傳入 callback,如 array_map()。在沒有 anonymous function 之前,我們只能這樣子使用 array_map()

1
2
3
4
5
6
7
8
9
// Named callback implementation
function incrementNumber ($number)
{
return $number + 1;
}

// Named callback usage
$numbersPlusOne = array_map('incrementNumber', [1,2,3]);
echo($numbersPlusOne);

array_map() 函式的第 1 個參數要求我們傳進一個 callback, 在 PHP 還沒有 anonymous function 之前,只能事先寫好一個函式,然後將函式名稱以字串的方式傳進 array_map() 第 1 個參數來實現 callback。4 4incrementNumber 是以字串的方式傳入,很容易誤會到底是字串還是 callback 函式,因此才有 PHP 5.3 的 anonymous function與 PHP 5.4 的 callable type hint。

這樣雖然可行,但通常 callback 只會用一次,為了這種只會用一次的函式另外獨立出一個函式似乎不妥,比較好的方式是改用 anonymous function。

1
2
3
4
5
$numbersPlusOne = array_map(function ($number) {
return $number + 1;
}, [1,2,3]);

print_r($numbersPlusOne);

由於 callback 只使用一次,將 anonymous function 直接寫在 array_map() 參數內除了更簡潔外,也解決了 PHP 以字串傳入 callback 的問題,程式可讀性也更高。

Closure


會將外層變數複製進 closure 函式,儘管函式結束,一樣可以讀取或寫入該外層變數。

使用 use 關鍵字
Pass by Reference

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function func(val) 
{

var foo = function () {
val++;
};

foo();

return val;
}

console.log(func(0));

// Result:
// 1

對於 JavaScript 而言,因為 foo 這個 closure 使用到外層的 val 變數,JavaScript 的 closure 會自動將外層的 val 變數以 pass by reference 方式傳進 closure,所以 val++ 會直接影響到外變數 val

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function func($val) 
{
$foo = function () use (&$val) {
$val++;
};

$foo();

return $val;
}

echo(func(0));

// Result:
// 1

對於 PHP 而言,因為 $foo 這個 closure 使用到外層的 $val 變數,PHP 的 closure 不會自動將外層的 $val變數傳進來,必須加上 use 關鍵字,並且將變數前面加上 & 以 pass by reference 方式傳進 closure,所以 $val++才會直接影響到外變數 val

Pass by Value

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function func(val) 
{

var foo = function () {
val2 = val;
val2++;
};

foo();

return val;
}

print(func(0));

// Result:
// 0

JavaScript 並沒有提供 pass by value 的好方法,所以只能自己使用類似 val2 = val 的蠢方法,避免影響到原來的 val

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function func($val) 
{
$foo = function () use ($val) {
$val++;
};

$foo();

return $val;
}

echo(func(0));

// Result:
// 0

PHP 因為預設就是 pass by value,所以只要拿掉 &,就是以 pass by value 的方式傳遞。5 5closure 還有一個進階的 bindTo() 用法,詳細請參考深入探討 bindTo()

JavaScript closure 與 PHP closure 的差異
  1. JavaScript 的 closure 可以直接使用外層變數,而 PHP 必須加上 use,明確指定要使用哪一個變數。
  2. JavaScript 對於 closure 的外層變數是以 pass by reference 處理,而 PHP 是 pass by value,若要 pass by reference,必須手動加上 &

Callable


為了避免因為 PHP 的弱型別本質,讓我們誤會傳入的是一般型別的參數,PHP 特別為傳入的 callback 參數加上 callable type hint,讓我們可以更明確得知要傳入的是 callback。

如PHP內建的 array_map() 就使用了 callable type hint。

1
array array_map ( callable $callback , array $array1 [, array $... ] )

最簡單的Callable

PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$myCallback = function () {
echo 'World';
};


function sayHello(callable $callback)
{

echo 'Hello ';
call_user_func($callback);
}

sayHello($myCallback);

// Result:
// Hello World

與 callback 的範例一樣,只是改寫成 callable。

第6行

1
2
3
4
5
function sayHello(callable $callback) 
{

echo 'Hello ';
call_user_func($callback);
}

$callback 之前多加了 callable type hint,這樣就能很清楚的了解要傳入的是一個 callback,而不是其他普通參數。

與其他程式語言比較


JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
function myClosure(hi) 
{

return function(name) {
return hi + " " + name;
}
}

var foo = myClosure("Hello");
console.log(foo("World"));

// Result:
// Hello World

  • 支援一級函式,函式可以指定給變數,函式可以直接傳遞至其他函式,也可以回傳函式。
  • 自動捕捉外層變數。

php

1
2
3
4
5
6
7
8
9
10
11
12
function myClosure($hi) 
{
return function($name) use ($hi) {
return sprintf("%s, %s", $hi, $name);
};
}

$foo = myClosure("Hello");
echo($foo("World"));

// Result:
// Hello World

  • 支援一級函式,函式可以指定給變數,函式可以直接傳遞至其他函式,也可以回傳函式。
  • 使用 use 關鍵字捕捉外層變數。

Conclusion


  • 以上雖然分別以 callback, anonymous funciton,closure 與 callable 說明,但實務上都是一起使用。
  • Closure 的出現使的 PHP 原本需要使用 callback 的函式有更精簡的寫法。
  • JavaScript 的 closure 不用使用 use,會自動將外層變數以 pass by reference 傳入,但 PHP 需手動使用 use 關鍵字指定欲傳入的外層變數,預設是 pass by value ,若要與 JavaScript 完全一樣,需加上 & 以 pass by reference 傳入。
2015-09-11