最近更新: 2009-11-26

PHP 5.3/6 新增功能 - Closures, const, and others

PHP 5.3 新增特性列表與本部落格的系列文章:

  • 名稱空間 (Namespace)
  • 延遲靜態繫結 (Late Static Bindings)
  • 新的魔術方法, __callStatic and __invoke.
  • 標記跳躍, Support for jump labels (limited goto) has been added.
    就是 goto ,忘了它吧。
  • HTTP 串流轉接器(HTTP stream wrapper) 現在將狀態碼 200 到 399 視為成功執行。我不曉得為什麼這會列在新功能中。這看來是為了改善 RESTful service 的支援。
  • 支援巢狀的例外處理。
  • 加入一個垃圾收集器,預設開啟。嗯... PHP 沒有垃圾收集功能嗎?Ok, 原本的垃圾回收機制清潔力不夠。 目前大多數 PHP 程式架構是處於一次性消耗的無狀態環境下,基本上我們都假設程式跑完後行程就自動結束,而行程中配置的資源也會被作業系統回收。 在這種情形下設計出來的 PHP 程式碼,直接搬到 application container 架構執行時,會出現資源佔用不放的問題。 所以這功能應該是為了將來發展 PHP 的 application container 而強化的機制。
  • 閉包、匿名函數(native Closures)(Lambda/Anonymous functions)。
  • 新的即席文件語法(Nowdoc syntax)。
  • 關鍵字 const 現在可用於類別定義之外。
  • 三元運算子(?:)有縮寫形式。

關於 Closures (匿名函數), Const, Nowdoc 等新功能,將於本文中說明。

Const

Const 同樣是定義常數的語法,但以往只能用在類別定義中。PHP 5.3 起讓它也能用在類別定義之外。這讓我們可以有一致化的常數表達方式,以後我們可以不用 define() 的寫法。但是 define() 仍然有一件事是 const 做不到的,就是代入變數作為常數名稱。

<?php
// before PHP 5.3

define('CONST1', 'const value 1');
if ( defined('CONST1') )
    echo CONST1, "\n";

// after PHP 5.3, you can use const

const CONST2 = 'const value 2';
if ( defined('CONST2') )
    echo CONST2, "\n";

//但是 define() 可以代入變數作為常數名稱。

$c = 3;
define("CONST$c", "const value $c");
if ( defined('CONST3') )
    echo CONST3, "\n";

?>

Nowdoc, 無跳脫處理的 Heredoc

Heredoc,一般譯為「即席文件」,是一種表達跨行且預先縮排過的字串語法。它對字串中的跳脫字元與變數處理方式,等同雙引號(")字串。而 Nowdoc 雖然有一個新名稱,但說穿了就是單引號(')字串版的 Heredoc 。 Nowdoc 不會特別處理跳脫字元與變數,就只是將之視為一般字元。

Nowdoc 的語法和 Heredoc 相似,差別在於起始的識別字要用單引號(')括起。為了強化表達力, PHP5.3 也建議大家使用 Heredoc 時,用雙引號(")括起起始的識別字。

<?php
$text = 'world';

$nowdoc = <<<'DOC1'
Hello $text\n

DOC1;

$heredoc = <<<"DOC2"
Hello $text\n

DOC2;

echo "---- NOWDOC ----\n";
echo $nowdoc;
echo "---- HEREDOC ----\n";
echo $heredoc;
echo "---- END ----\n";
?>

注意輸出結果, Nowdoc 會原原本本地顯示 $text\n

---- NOWDOC ----
Hello $text\n
---- HEREDOC ----
Hello world

---- END ----

Shorthand Ternary

縮寫版的 ?: 運算子。這是針對一個還算常見的程式碼形式所提供的縮寫法,它可以讓我們省略一個重複的表達式。我們三不五時會用到「當 a 為真時回傳 a ,否則回傳 b 」的式子,即 a ? a : b 。這個縮寫法可以省略「回傳a」的表達式,自動回傳第一個式子的值。

<?php
$a = '123';
$b = '456';

$c = $a ? $a : $b;
echo $c, "\n";

//after PHP 5.3

// (expr1) ? (expr2) : (expr3)

// 當 expr2 省略時,PHP 自動以 expr1 代入。


$c = $a ?: $b;
echo $c, "\n";
?>

Closures

Closures ,數學常用譯名為「閉包」或「閉鎖」,我戲稱為「封絕」。不過我個人更常使用「匿名函數(Anonymous function)」這一詞彙。各位可以看看我先前發布的匿名函數相關文章

以往 PHP 可以使用 create_function() 建立一個匿名函數,但是寫法非常不方便,基本上我們不會想用以前的方式來寫 PHP 的匿名函數,就算寫了也不一定有我們預期的結果。

PHP 5.3 終於提供了一個更好的匿名函數語法,這個語法與 JavaScript 的語法一致,我想各位應該不會感到學習困難。只有一點差異, JavaScript 的匿名函數將共享上層活動領域(scope)的變數;但是 PHP 的匿名函數則不會共享上層活動領域的變數,你必須使用 use 關鍵字將外層變數導入匿名函數內

先來看一段 JavaScript 的寫法。

// JavaScript anonymous function

i = 1

call = function(j) {
    print(i + j); //共享外層的變數 i

}

call(2);

(function(j){
    print(i + j);
})(2)

PHP 5.3 的寫法則是:

<?php
// PHP anonymous function

$i = 1;

$call = function($j) {
    echo $i + $j, "\n"; //Notice: Undefined variable: i

}; //注意結束的分號(;)


$call(2);

$call = function($j) use ($i) {// use $i of outside

    echo $i + $j, "\n";
};

$call(2);

/*PHP 不支持這種寫法
(function($j) use ($i) {
    echo $i + $j, "\n";
})(2);
*/
?>
Notice: Undefined variable: i in anonymous_function.php on line 6
2
3

接下來示範的是一個可以讓使用者傳入匿名函數的程式。看過PHP的中介編程與反射能力示範的人,應該會對下列程式碼感到似曾相識。

<?php
class Data {
    protected $id;
    protected $title;

    public function __construct() {
        $this->id = 1;
        $this->title = 'abc';
    }

//  明確的型別宣告反而會錯誤... Orz

//  public function each(callback $callback) { 

    public function each($callback) {
        $rd = new ReflectionObject($this);
        foreach ($rd->getProperties() as $property) {
            $key = $property->getName();
            $callback($key, $this->$key);
        }
    }
}

$d = new Data;

$d->each(function($k, $v){
    echo "$k = $v\n";
});
?>

基本上, PHP 提供的函數中,只要是寫明 $callback 的,都可以用匿名函數作為參數。

嚴格來說是 callback 型別的參數就可以用匿名函數。但目前為止(本文撰寫時最新版本為PHP 5.3.1)似乎有一個 bug ,如果我將參數列的 $callback 宣告為 callback 型別,PHP 反而會擲出一個錯誤,抱怨我傳給它的是一個 Closure 實體而不是 callback 實體: Catchable fatal error: Argument 1 passed to Data::each() must be an instance of callback, instance of Closure given.。這是 PHP 從 Java 學到的缺點,型別紊亂。所幸 PHP 可以忽略參數的型別宣告,故而我們不必像 Java 程序員那樣精通如何把方形塞進圓洞的技巧。省略參數的型別宣告,會變得較美好。


create_function 的潛規則

回覆 Chaosrx 的內容。

  1. PHP 5.3 新增了一個內建類別,叫做 Closure 。 Closure 是真實存在的類別,不像 callback 只是文件上存在的類別(別問我為何 callback 不是內建類別,我也不知道)。
    所有採用新式匿名函數語法建立的匿名函數,都是 Closure 的實例。所以宣告 public function each(Closure $callback) {...} 不會發生錯誤,這是很正常的。
  2. 宣告 public function each(Closure $callback) {...} ,並不需要配合 use Closure;
    事實上,你直接這樣寫的話,PHP 還會發出一個警告訊息,告訴你 use Closure; 沒有任何作用。因為你沒有定義任何一個叫做 Closure 的名稱空間。
  3. Chaosrx 看到的 sources ,一定在某處定義了名稱叫 Closure 的名稱空間,所以它才會寫 use Closure;
    注意,「use Closure」與「宣告函數的參數型態為 Closure」是不相干的。
  4. 當你明確地宣告參數型態為 Closure 時,你只能傳遞新式語法建立的匿名函數,而不能傳舊式的 create_function() 建立的匿名函數。

上述答覆的最後一點,牽涉到 PHP 處理 create_function()潛規則

首先,我們來看看新、舊兩種語法產生的匿名函數碰上 Closure 型態宣告時會發生什麼事?

<?php
function test1(Closure $f) {
    $f();
}

test1( function(){
    echo "new anonymous function\n";
});

test1( create_function('',
    'echo "old anonymous function\n";'
)); // Catchable fatal error!

?>

瞧,PHP 擲出錯誤了。它說我們用 create_function() (舊式匿名函數語法) 建立的東西並不是一個 Closure 的實例。

那麼 create_function() 建立的是什麼東西?

根據 PHP Manual 所說,是 string 。見鬼。我寫了十年PHP,從沒看過 'echo "hello";'() 可以執行。

很顯然, create_function() 回傳的東西絕對不是單純的字串,它回傳的字串其實是匿名函數的名稱。事實上, PHP 建立了一個特殊的潛規則去處理 create_function() 回傳的字串。那個潛規則就是,所有 create_function() 建立的匿名函數,內部的函數名稱都是以 "\0lambda_" 為首,再接上編號;例如 "\0lambda_1"create_function() 回傳的字串就是這個名稱。

當 PHP 碰到 $s();$s 為字串的敘述時,它會根據字串內容去查函數表。以 "\0lambda_" 為首的,自然就是 lambda 函數。

當我們知道這個潛規則時,我們就可以玩一個非常有趣的把戲。如下所示範:

<?php
function test2( $f ) {
    $f();
}

test2( function(){
    echo "new anonymous function\n";
});

// Create our first lambda, so it will be named "\0lambda_1".

$lambda = create_function('',
    'echo "old anonymous function\n";'
);

var_dump($lambda); //string(9) "lambda_1"


test2( "\0lambda_1" ); //It works!


?>

只要我們能夠控制 create_function() 執行的順序,我們還可以玩隨機呼叫不同的 lambda 。

<?php

$a = array(
    'black',
    'white',
    'blue',
    'red',
    'green'
);

$lambda_base = create_function('', ''); //empty lambda


preg_match('/\d+$/', $lambda_base, $m);
$lambda_base = $m[0];

foreach ($a as $c) {
    create_function('',
        'echo "paint ' . $c . '\n";'
    );
    //create lambda ($lambda_base + 1) to ($lambda_base + 5).

}

for ($i = 0; $i < 10; $i++) {
    $n = rand($lambda_base + 1, $lambda_base + count($a) );
    $lambda = "\0lambda_$n";
    $lambda(); //every result is difference.

}

?>

這個把戲看來有趣,其實用途不大。既然 PHP 5.3 正式提供了 Closure 類別,那麼 create_function() 的潛規則便沒有必要繼續存在。 PHP 應該早點將 create_function() 回傳的內容改成 Closure 的實例,才是正途。

相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/10833753.html

樂多舊回應
未留名 (#comment-20111387)
Thu, 26 Nov 2009 10:08:01 +0800
callback 是個假型別喔,是給文件用的,實際上是不存在的... (請參考: http://www.php.net/manual/en/language.pseudo-types.php)
callback 在 5.3 以前其實是指以下型別: string, array
未留名 (#comment-20113701)
Thu, 26 Nov 2009 19:25:06 +0800
在動態語言的環境中,型別宣告本來就是參考文件一般的存在。

問題是,為什麼加上這個假型別後,反而妨礙了原本可運作的程式。

Manual 的同一頁,最後一句說「As of PHP 5.3.0 it is possible to also pass a closure to a callback parameter. 」。所有 PHP 函數中,關於 $callback 參數的型別宣告都是 callback。比照 PHP 的官方示範,我們自己設計的函數參數宣告 callback 型別後應該也一樣能用,這種預期是很合理也很正常的。但實際上是「You can not pass」。
對我而言,這就是 bug 。

另外,我特地查了我本機中 CHM 版本的 PHP manual ,在 PHP5.3之前,$callback參數的型別就一直是寫 callback.例如

mixed preg_replace_callback ( mixed $pattern , callback $callback , mixed $subject [, int $limit= -1 [, int &$count ]] )

CHM版本是 2009-4-17, PHP 5.3還沒 release.

未留名 (#comment-20125419)
Mon, 30 Nov 2009 17:13:12 +0800
所以才會說它只是給文件用的...這點官方一直說得很清楚耶...就像 mixed 也不是真的型態一樣的道理...
未留名 (#comment-20222933)
Sun, 27 Dec 2009 06:07:21 +0800
我在一個 Open Source 的程式碼內看到
關於
public function each(callback $callback)
宣告錯誤的地方

該程式是在程式檔開頭先加入
use Closure;

然後宣告成
public function each(Closure $callback) {...}
未留名 (#comment-20230337)
Sun, 27 Dec 2009 06:15:38 +0800
關於 Chaosrx 說的事,我回覆的內容有點多。

還牽涉到 create_function() 有趣的潛規則。

我補到正文的後面去了。