匿名函數在編程語言中出現的比較早,最早出現在Lisp語言中,隨后很多的編程語言都開始有這個功能了,目前使用比較廣泛的Javascript以及C#,PHP直到5.3才開始真正支持匿名函數,C++的新標準[C++0x](http://en.wikipedia.org/wiki/C%2B%2B0x)也開始支持了。
匿名函數是一類不需要指定標示符,而又可以被調用的函數或子例程,匿名函數可以方便的作為參數傳遞給其他函數,最常見應用是作為回調函數。
## 閉包(Closure)[]()
說到匿名函數,就不得不提到閉包了,閉包是詞法閉包(Lexical Closure)的簡稱,是引用了自由變量的函數,這個被應用的自由變量將和這個函數一同存在,即使離開了創建它的環境也一樣,所以閉包也可認為是有函數和與其相關引用組合而成的實體。在一些語言中,在函數內定義另一個函數的時候,如果內部函數引用到外部函數的變量,則可能產生閉包。在運行外部函數時,一個閉包就形成了。
這個詞和匿名函數很容易被混用,其實這是兩個不同的概念,這可能是因為很多語言實現匿名函數的時候允許形成閉包。
## 使用create_function()創建"匿名"函數[]()
前面提到PHP5.3中才才開始正式支持匿名函數,說到這里可能會有細心讀者有意見了,因為有個函數是可以生成匿名函數的: create_function函數,在手冊里可以查到這個[函數](http://cn2.php.net/create_function)在PHP4.1和PHP5中就有了,這個函數通常也能作為匿名回調函數使用,例如如下:
<?php
?
$array = array(1, 2, 3, 4);
array_walk($array, create_function('$value', 'echo $value'));
這段代碼只是將數組中的值依次輸出,當然也能做更多的事情。 那為什么這不算真正的匿名函數呢,我們先看看這個函數的返回值,這個函數返回一個字符串,通常我們可以像下面這樣調用一個函數:
<?php
?
function a() {
echo 'function a';
}
?
$a = 'a';
$a();
我們在實現回調函數的時候也可以采用這樣的方式,例如:
<?php
?
function do_something($callback) {
// doing
# ...
?
// done
$callback();
}
這樣就能實現在函數do_something()執行完成之后調用$callback指定的函數。回到create_function函數的返回值:函數返回一個唯一的字符串函數名,出現錯誤的話則返回FALSE。這么說這個函數也只是動態的創建了一個函數,而這個函數是**有函數名**的,也就是說,其實這并不是匿名的。只是創建了一個全局唯一的函數而已。
<?php
$func = create_function('', 'echo "Function created dynamic";');
echo $func; // lambda_1
?
$func(); // Function created dynamic
?
$my_func = 'lambda_1';
$my_func(); // 不存在這個函數
lambda_1(); // 不存在這個函數
上面這段代碼的前面很好理解,create_function就是這么用的,后面通過函數名來調用卻失敗了,這就有些不好理解了,php是怎么保證這個函數是全局唯一的? lambda_1看起來也是一個很普通的函數名,如果我們先定義一個叫做lambda_1的函數呢?這里函數的返回字符串會是lambda_2,它在創建函數的時候會檢查是否這個函數是否存在知道找到合適的函數名,但如果我們在create_function之后定義一個叫做lambda_1的函數會怎么樣呢? 這樣就出現函數重復定義的問題了,這樣的實現恐怕不是最好的方法,實際上如果你真的定義了名為lambda_1的函數也是不會出現我所說的問題的。這究竟是怎么回事呢?上面代碼的倒數2兩行也說明了這個問題,實際上并沒有定義名為lambda_1的函數。
也就是說我們的lambda_1和create_function返回的lambda_1并不是一樣的!? 怎么會這樣呢? 那只能說明我們沒有看到實質,只看到了表面,表面是我們在echo的時候輸出了lambda_1,而我們的lambda_1是我們自己敲入的. 我們還是使用[debug_zval_dump](http://cn.php.net/manual/en/function.debug-zval-dump.php)函數來看看吧。
<?php
$func = [create_function](http://www.php.net/create_function)('', 'echo "Hello";');
?
$my_func_name = 'lambda_1';
[debug_zval_dump](http://www.php.net/debug_zval_dump)($func); // string(9) "lambda_1" refcount(2)
[debug_zval_dump](http://www.php.net/debug_zval_dump)($my_func_name); // string(8) "lambda_1" refcount(2)
看出來了吧,他們的長度居然不一樣,長度不一樣也即是說不是同一個函數,所以我們調用的函數當然是不存在的,我們還是直接看看create_function函數到底都做了些什么吧。該實現見: $PHP_SRC/Zend/zend_builtin_functions.c
#define LAMBDA_TEMP_FUNCNAME "__lambda_func"
?
ZEND_FUNCTION(create_function)
{
// ... 省去無關代碼
function_name = (char *) emalloc(sizeof("0lambda_")+MAX_LENGTH_OF_LONG);
function_name[0] = '\0'; // <--- 這里
do {
function_name_length = 1 + sprintf(function_name + 1, "lambda_%d", ++EG(lambda_count));
} while (zend_hash_add(EG(function_table), function_name, function_name_length+1, &new_function, sizeof(zend_function), NULL)==FAILURE);
zend_hash_del(EG(function_table), LAMBDA_TEMP_FUNCNAME, sizeof(LAMBDA_TEMP_FUNCNAME));
RETURN_STRINGL(function_name, function_name_length, 0);
}
該函數在定義了一個函數之后,給函數起了個名字,它將函數名的第一個字符變為了'\0'也就是空字符,然后在函數表中查找是否已經定義了這個函數,如果已經有了則生成新的函數名, 第一個字符為空字符的定義方式比較特殊, 因為在用戶代碼中無法定義出這樣的函數, 也就不存在命名沖突的問題了,這也算是種取巧(tricky)的做法,在了解到這個特殊的函數之后,我們其實還是可以調用到這個函數的, 只要我們在函數名前加一個空字符就可以了, chr()函數可以幫我們生成這樣的字符串, 例如前面創建的函數可以通過如下的方式訪問到:
<?php
?
$my_func = [chr](http://www.php.net/chr)(0) . "lambda_1";
$my_func(); // Hello
這種創建"匿名函數"的方式有一些缺點:
1. 函數的定義是通過字符串動態eval的, 這就無法進行基本的語法檢查;
1. 這類函數和普通函數沒有本質區別, 無法實現閉包的效果.
## 真正的匿名函數[]()
在PHP5.3引入的眾多功能中, 除了匿名函數還有一個特性值得講講: 新引入的[__invoke](http://www.php.net/manual/en/language.oop5.magic.php#language.oop5.magic.invoke)[魔幻方法](http://www.php.net/manual/en/language.oop5.magic.php)。
### __invoke魔幻方法[]()
這個魔幻方法被調用的時機是: 當一個對象當做函數調用的時候, 如果對象定義了__invoke魔幻方法則這個函數會被調用,這和C++中的操作符重載有些類似, 例如可以像下面這樣使用:
<?php
class Callme {
public function __invoke($phone_num) {
[echo](http://www.php.net/echo) "Hello: $phone_num";
}
}
?
$call = new Callme();
$call(13810688888); // "Hello: 13810688888
### 匿名函數的實現[]()
前面介紹了將對象作為函數調用的方法, 聰明的你可能想到在PHP實現匿名函數的方法了,PHP中的匿名函數就的確是通過這種方式實現的。我們先來驗證一下:
<?php
$func = function() {
[echo](http://www.php.net/echo) "Hello, anonymous function";
}
?
[echo](http://www.php.net/echo) [gettype](http://www.php.net/gettype)($func); // object
[echo](http://www.php.net/echo) [get_class](http://www.php.net/get_class)($func); // Closure
原來匿名函數也只是一個普通的類而已。熟悉Javascript的同學對匿名函數的使用方法很熟悉了,PHP也使用和Javascript類似的語法來[定義](http://cn.php.net/manual/en/functions.anonymous.php), 匿名函數可以賦值給一個變量, 因為匿名函數其實是一個類實例, 所以能復制也是很容易理解的, 在Javascript中可以將一個匿名函數賦值給一個對象的屬性, 例如:
var a = {};
a.call = function() {alert("called");}
a.call(); // alert called
這在Javascript中很常見, 但在PHP中這樣并不可以, 給對象的屬性復制是不能被調用的, 這樣使用將會導致類尋找類中定義的方法,在PHP中屬性名和定義的方法名是可以重復的, 這是由PHP的類模型所決定的, 當然PHP在這方面是可以改進的, 后續的版本中可能會允許這樣的調用,這樣的話就更容易靈活的實現一些功能了。目前想要實現這樣的效果也是有方法的: 使用另外一個魔幻方法__call(),至于怎么實現就留給各位讀者當做習題吧。
### 閉包的使用[]()
PHP使用閉包(Closure)來實現匿名函數, 匿名函數最強大的功能也就在匿名函數所提供的一些動態特性以及閉包效果,匿名函數在定義的時候如果需要使用作用域外的變量需要使用如下的語法來實現:
<?php
$name = 'TIPI Team';
$func = function() use($name) {
[echo](http://www.php.net/echo) "Hello, $name";
}
?
$func(); // Hello TIPI Team
這個use語句看起來挺別扭的, 尤其是和Javascript比起來, 不過這也應該是PHP-Core綜合考慮才使用的語法, 因為和Javascript的作用域不同, PHP在函數內定義的變量默認就是局部變量, 而在Javascript中則相反,除了顯式定義的才是局部變量, PHP在變異的時候則無法確定變量是局部變量還是上層作用域內的變量, 當然也可能有辦法在編譯時確定,不過這樣對于語言的效率和復雜性就有很大的影響。
這個語法比較直接,如果需要訪問上層作用域內的變量則需要使用use語句來申明, 這樣也簡單易讀,說到這里, 其實可以使用use來實現類似global語句的效果。
匿名函數在每次執行的時候都能訪問到上層作用域內的變量, 這些變量在匿名函數被銷毀之前始終保存著自己的狀態,例如如下的例子:
<?php
function getCounter() {
$i = 0;
return function() use($i) { // 這里如果使用引用傳入變量: use(&$i)
[echo](http://www.php.net/echo) ++$i;
};
}
?
$counter = getCounter();
$counter(); // 1
$counter(); // 1
和Javascript中不同,這里兩次函數調用并沒有使$i變量自增,默認PHP是通過拷貝的方式傳入上層變量進入匿名函數,如果需要改變上層變量的值則需要通過引用的方式傳遞。所以上面得代碼沒有輸出`1, 2`而是`1,1`。
### 閉包的實現[]()
前面提到匿名函數是通過閉包來實現的, 現在我們開始看看閉包(類)是怎么實現的。匿名函數和普通函數除了是否有變量名以外并沒有區別,閉包的實現代碼在$PHP_SRC/Zend/zend_closure.c。匿名函數"對象化"的問題已經通過Closure實現, 而對于匿名是怎么樣訪問到創建該匿名函數時的變量的呢?
例如如下這段代碼:
<?php
$i=100;
$counter = function() use($i) {
debug_zval_dump($i);
};
?
$counter();
通過VLD來查看這段編碼編譯什么樣的opcode了
$ php -dvld.active=1 closure.php
?
vars: !0 = $i, !1 = $counter
# * op fetch ext return operands
------------------------------------------------------------------------
0 > ASSIGN !0, 100
1 ZEND_DECLARE_LAMBDA_FUNCTION '%00%7Bclosure
2 ASSIGN !1, ~1
3 INIT_FCALL_BY_NAME !1
4 DO_FCALL_BY_NAME 0
5 > RETURN 1
?
function name: {closure}
number of ops: 5
compiled vars: !0 = $i
line # * op fetch ext return operands
--------------------------------------------------------------------------------
3 0 > FETCH_R static $0 'i'
1 ASSIGN !0, $0
4 2 SEND_VAR !0
3 DO_FCALL 1 'debug_zval_dump'
5 4 > RETURN null
上面根據情況去掉了一些無關的輸出, 從上到下, 第1開始將100賦值給!0也就是變量$i, 隨后執行ZEND_DECLARE_LAMBDA_FUNCTION,那我們去相關的opcode執行函數中看看這里是怎么執行的, 這個opcode的處理函數位于$PHP_SRC/Zend/zend_vm_execute.h中:
static int ZEND_FASTCALL ZEND_DECLARE_LAMBDA_FUNCTION_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)
{
zend_op *opline = EX(opline);
zend_function *op_array;
?
if (zend_hash_quick_find(EG(function_table), Z_STRVAL(opline->op1.u.constant), Z_STRLEN(opline->op1.u.constant), Z_LVAL(opline->op2.u.constant), (void *) &op_arra
y) == FAILURE ||
op_array->type != ZEND_USER_FUNCTION) {
zend_error_noreturn(E_ERROR, "Base lambda function for closure not found");
}
?
zend_create_closure(&EX_T(opline->result.u.var).tmp_var, op_array TSRMLS_CC);
?
ZEND_VM_NEXT_OPCODE();
}
該函數調用了zend_create_closure()函數來創建一個閉包對象, 那我們繼續看看位于$PHP_SRC/Zend/zend_closures.c的zend_create_closure()函數都做了些什么。
ZEND_API void zend_create_closure(zval *res, zend_function *func TSRMLS_DC)
{
zend_closure *closure;
?
object_init_ex(res, zend_ce_closure);
?
closure = (zend_closure *)zend_object_store_get_object(res TSRMLS_CC);
?
closure->func = *func;
?
if (closure->func.type == ZEND_USER_FUNCTION) { // 如果是用戶定義的匿名函數
if (closure->func.op_array.static_variables) {
HashTable *static_variables = closure->func.op_array.static_variables;
?
// 為函數申請存儲靜態變量的哈希表空間
ALLOC_HASHTABLE(closure->func.op_array.static_variables);
zend_hash_init(closure->func.op_array.static_variables, zend_hash_num_elements(static_variables), NULL, ZVAL_PTR_DTOR, 0);
?
// 循環當前靜態變量列表, 使用zval_copy_static_var方法處理
zend_hash_apply_with_arguments(static_variables TSRMLS_CC, (apply_func_args_t)zval_copy_static_var, 1, closure->func.op_array.static_variables);
}
(*closure->func.op_array.refcount)++;
}
?
closure->func.common.scope = NULL;
}
如上段代碼注釋中所說, 繼續看看zval_copy_static_var()函數的實現:
static int zval_copy_static_var(zval **p TSRMLS_DC, int num_args, va_list args, zend_hash_key *key)
{
HashTable *target = va_arg(args, HashTable*);
zend_bool is_ref;
?
// 只對通過use語句類型的靜態變量進行取值操作, 否則匿名函數體內的靜態變量也會影響到作用域之外的變量
if (Z_TYPE_PP(p) & (IS_LEXICAL_VAR|IS_LEXICAL_REF)) {
is_ref = Z_TYPE_PP(p) & IS_LEXICAL_REF;
?
if (!EG(active_symbol_table)) {
zend_rebuild_symbol_table(TSRMLS_C);
}
// 如果當前作用域內沒有這個變量
if (zend_hash_quick_find(EG(active_symbol_table), key->arKey, key->nKeyLength, key->h, (void **) &p) == FAILURE) {
if (is_ref) {
zval *tmp;
?
// 如果是引用變量, 則創建一個臨時變量一邊在匿名函數定義之后對該變量進行操作
ALLOC_INIT_ZVAL(tmp);
Z_SET_ISREF_P(tmp);
zend_hash_quick_add(EG(active_symbol_table), key->arKey, key->nKeyLength, key->h, &tmp, sizeof(zval*), (void**)&p);
} else {
// 如果不是引用則表示這個變量不存在
p = &EG(uninitialized_zval_ptr);
zend_error(E_NOTICE,"Undefined variable: %s", key->arKey);
}
} else {
// 如果存在這個變量, 則根據是否是引用, 對變量進行引用或者復制
if (is_ref) {
SEPARATE_ZVAL_TO_MAKE_IS_REF(p);
} else if (Z_ISREF_PP(p)) {
SEPARATE_ZVAL(p);
}
}
}
if (zend_hash_quick_add(target, key->arKey, key->nKeyLength, key->h, p, sizeof(zval*), NULL) == SUCCESS) {
Z_ADDREF_PP(p);
}
return ZEND_HASH_APPLY_KEEP;
}
這個函數作為一個回調函數傳遞給`zend_hash_apply_with_arguments()`函數, 每次讀取到hash表中的值之后由這個函數進行處理,而這個函數對所有use語句定義的變量值賦值給這個匿名函數的靜態變量, 這樣匿名函數就能訪問到use的變量了。
- 第一章 準備工作和背景知識
- 第一節 環境搭建
- 第二節 源碼結構、閱讀代碼方法
- 第三節 常用代碼
- 第四節 小結
- 第二章 用戶代碼的執行
- 第一節 生命周期和Zend引擎
- 第二節 SAPI概述
- Apache模塊
- 嵌入式
- FastCGI
- 第三節 PHP腳本的執行
- 詞法分析和語法分析
- opcode
- opcode處理函數查找
- 第四節 小結
- 第三章 變量及數據類型
- 第一節 變量的結構和類型
- 哈希表(HashTable)
- PHP的哈希表實現
- 鏈表簡介
- 第二節 常量
- 第三節 預定義變量
- 第四節 靜態變量
- 第五節 類型提示的實現
- 第六節 變量的生命周期
- 變量的賦值和銷毀
- 變量的作用域
- global語句
- 第七節 數據類型轉換
- 第八節 小結
- 第四章 函數的實現
- 第一節 函數的內部結構
- 函數的內部結構
- 函數間的轉換
- 第二節 函數的定義,傳參及返回值
- 函數的定義
- 函數的參數
- 函數的返回值
- 第三節 函數的調用和執行
- 第四節 匿名函數及閉包
- 第五節 小結
- 第五章 類和面向對象
- 第一節 類的結構和實現
- 第二節 類的成員變量及方法
- 第三節 訪問控制的實現
- 第四節 類的繼承,多態及抽象類
- 第五節 魔術方法,延遲綁定及靜態成員
- 第六節 PHP保留類及特殊類
- 第七節 對象
- 第八節 命名空間
- 第九節 標準類
- 第十節 小結
- 第六章 內存管理
- 第一節 內存管理概述
- 第二節 PHP中的內存管理
- 第三節 內存使用:申請和銷毀
- 第四節 垃圾回收
- 新的垃圾回收
- 第五節 內存管理中的緩存
- 第六節 寫時復制(Copy On Write)
- 第七節 內存泄漏
- 第八節 小結
- 第七章 Zend虛擬機
- 第一節 Zend虛擬機概述
- 第二節 語法的實現
- 詞法解析
- 語法分析
- 實現自己的語法
- 第三節 中間代碼的執行
- 第四節 PHP代碼的加密解密
- 第五節 小結
- 第八章 線程安全
- 第二節 線程,進程和并發
- 第三節 PHP中的線程安全
- 第九章 錯誤和異常處理
- 第十章 輸出緩沖
- 第十六章 PHP語言特性的實現
- 第一節 循環語句
- foreach的實現
- 第二十章 怎么樣系列(how to)
- 附錄
- 附錄A PHP及Zend API
- 附錄B PHP的歷史
- 附錄C VLD擴展使用指南
- 附錄D 怎樣為PHP貢獻
- 附錄E phpt測試文件說明
- 附錄F PHP5.4新功能升級解析
- 附錄G:re2c中文手冊