PHP匿名函數及閉包

PHP匿名函數及閉包

 
 

目錄(?)[+]php

[iefreer] 轉載一篇對PHP閉包語法講解比較深刻到位的文章,後續還會轉一篇這些新語法如何巧妙應用的文章。編程

 

匿名函數在編程語言中出現的比較早,最先出如今Lisp語言中,隨後不少的編程語言都開始有這個功能了,數組

目前使用比較普遍的Javascript以及C#,PHP直到5.3纔開始真正支持匿名函數,C++的新標準C++0x也開始支持了。閉包

匿名函數是一類不須要指定標示符,而又能夠被調用的函數或子例程,匿名函數能夠方便的做爲參數傳遞給其餘函數,最多見應用是做爲回調函數。app

閉包(Closure)

說 到匿名函數,就不得不提到閉包了,閉包是詞法閉包(Lexical Closure)的簡稱,是引用了自由變量的函數,這個被應用的自由變量將和這個函數一同存在,即便離開了建立它的環境也同樣,因此閉包也可認爲是有函數 和與其相關引用組合而成的實體。在一些語言中,在函數內定義另外一個函數的時候,若是內部函數引用到外部函數的變量,則可能產生閉包。在運行外部函數時,一 個閉包就造成了。編程語言

這個詞和匿名函數很容易被混用,其實這是兩個不一樣的概念,這多是由於不少語言實現匿名函數的時候容許造成閉包。函數

使用create_function()建立"匿名"函數

前面提到PHP5.3中才纔開始正式支持匿名函數,說到這裏可能會有細心讀者有意見了,由於有個函數是能夠生成匿名函數的: create_function函數,在手冊裏能夠查到這個函數在PHP4.1和PHP5中就有了,這個函數一般也能做爲匿名回調函數使用,例如以下:oop

  1. <?php  
  2.    
  3. $array = array(1, 2, 3, 4);  
  4. array_walk($array, create_function('$value', 'echo $value'));  


這段代碼只是將數組中的值依次輸出,固然也能作更多的事情。 那爲何這不算真正的匿名函數呢,咱們先看看這個函數的返回值,這個函數返回一個字符串,一般咱們能夠像下面這樣調用一個函數:fetch

  1. <?php  
  2.    
  3. function a() {  
  4.     echo 'function a';  
  5. }  
  6.    
  7. $a = 'a';  
  8. $a();  

 

咱們在實現回調函數的時候也能夠採用這樣的方式,例如:ui

  1. <?php  
  2.    
  3. function do_something($callback) {  
  4.     // doing  
  5.     # ...  
  6.    
  7.     // done  
  8.     $callback();  
  9. }  

 

這樣就能實如今函數do_something()執行完成以後調用$callback指定的函數。回到create_function函數的返回值:函數返回一個惟一的字符串函數名,出現錯誤的話則返回FALSE。這麼說這個函數也只是動態的建立了一個函數,而這個函數是有函數名的,也就是說,其實這並非匿名的。只是建立了一個全局惟一的函數而已。

  1. <?php  
  2. $func = create_function('', 'echo "Function created dynamic";');  
  3. echo $func; // lambda_1  
  4.    
  5. $func();    // Function created dynamic  
  6.    
  7. $my_func = 'lambda_1';  
  8. $my_func(); // 不存在這個函數  
  9. 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函數來看看吧。

  1. <?php  
  2. $func = create_function('', 'echo "Hello";');  
  3.    
  4. $my_func_name = 'lambda_1';  
  5. debug_zval_dump($func);         // string(9) "lambda_1" refcount(2)  
  6. debug_zval_dump($my_func_name); // string(8) "lambda_1" refcount(2)  

 

看 出來了吧,他們的長度竟然不同,長度不同也便是說不是同一個函數,因此咱們調用的函數固然是不存在的,咱們仍是直接看看 create_function函數到底都作了些什麼吧。該實現見: $PHP_SRC/Zend/zend_builtin_functions.c

  1. #define LAMBDA_TEMP_FUNCNAME    "__lambda_func"  
  2.    
  3. ZEND_FUNCTION(create_function)  
  4. {  
  5.     // ... 省去無關代碼  
  6.     function_name = (char *) emalloc(sizeof("0lambda_")+MAX_LENGTH_OF_LONG);  
  7.     function_name[0] = '\0';  // <--- 這裏  
  8.     do {  
  9.         function_name_length = 1 + sprintf(function_name + 1, "lambda_%d", ++EG(lambda_count));  
  10.     } while (zend_hash_add(EG(function_table), function_name, function_name_length+1, &new_function, sizeof(zend_function), NULL)==FAILURE);  
  11.     zend_hash_del(EG(function_table), LAMBDA_TEMP_FUNCNAME, sizeof(LAMBDA_TEMP_FUNCNAME));  
  12.     RETURN_STRINGL(function_name, function_name_length, 0);  
  13. }  

 

該 函數在定義了一個函數以後,給函數起了個名字,它將函數名的第一個字符變爲了'\0'也就是空字符,而後在函數表中查找是否已經定義了這個函數,若是已經 有了則生成新的函數名, 第一個字符爲空字符的定義方式比較特殊, 由於在用戶代碼中沒法定義出這樣的函數, 也就不存在命名衝突的問題了,這也算是種取巧(tricky)的作法,在瞭解到這個特殊的函數以後,咱們其實仍是能夠調用到這個函數的, 只要咱們在函數名前加一個空字符就能夠了, chr()函數能夠幫咱們生成這樣的字符串, 例如前面建立的函數能夠經過以下的方式訪問到:

  1. <?php  
  2.    
  3. $my_func = chr(0) . "lambda_1";  
  4. $my_func(); // Hello  

 

這種建立"匿名函數"的方式有一些缺點:

  1. 函數的定義是經過字符串動態eval的, 這就沒法進行基本的語法檢查;
  2. 這類函數和普通函數沒有本質區別, 沒法實現閉包的效果.

真正的匿名函數

在PHP5.3引入的衆多功能中, 除了匿名函數還有一個特性值得講講: 新引入的__invoke 魔幻方法

__invoke魔幻方法

這個魔幻方法被調用的時機是: 當一個對象當作函數調用的時候, 若是對象定義了__invoke魔幻方法則這個函數會被調用,這和C++中的操做符重載有些相似, 例如能夠像下面這樣使用:

  1. <?php  
  2. class Callme {  
  3.     public function __invoke($phone_num) {  
  4.         echo "Hello: $phone_num";  
  5.     }  
  6. }  
  7.    
  8. $call = new Callme();  
  9. $call(13810688888); // "Hello: 13810688888  

 

匿名函數的實現

前面介紹了將對象做爲函數調用的方法, 聰明的你可能想到在PHP實現匿名函數的方法了,PHP中的匿名函數就的確是經過這種方式實現的。咱們先來驗證一下:

  1. <?php  
  2. $func = function() {  
  3.     echo "Hello, anonymous function";  
  4. }  
  5.    
  6. echo gettype($func);    // object  
  7. echo get_class($func);  // Closure  

 

原來匿名函數也只是一個普通的類而已。熟悉Javascript的同窗對匿名函數的使用方法很熟悉了,PHP也使用和Javascript相似的語法來定義, 匿名函數能夠賦值給一個變量, 由於匿名函數實際上是一個類實例, 因此能複製也是很容易理解的, 在Javascript中能夠將一個匿名函數賦值給一個對象的屬性, 例如:

  1. var a = {};  
  2. a.call = function() {alert("called");}  
  3. a.call(); // alert called  

 

這 在Javascript中很常見, 但在PHP中這樣並不能夠, 給對象的屬性複製是不能被調用的, 這樣使用將會致使類尋找類中定義的方法,在PHP中屬性名和定義的方法名是能夠重複的, 這是由PHP的類模型所決定的, 固然PHP在這方面是能夠改進的, 後續的版本中可能會容許這樣的調用,這樣的話就更容易靈活的實現一些功能了。目前想要實現這樣的效果也是有方法的: 使用另一個魔幻方法__call(),至於怎麼實現就留給各位讀者當作習題吧。

閉包的使用

PHP使用閉包(Closure)來實現匿名函數, 匿名函數最強大的功能也就在匿名函數所提供的一些動態特性以及閉包效果,匿名函數在定義的時候若是須要使用做用域外的變量須要使用以下的語法來實現:

  1. <?php  
  2. $name = 'TIPI Team';  
  3. $func = function() use($name) {  
  4.     echo "Hello, $name";  
  5. }  
  6.    
  7. $func(); // Hello TIPI Team  

 

這 個use語句看起來挺彆扭的, 尤爲是和Javascript比起來, 不過這也應該是PHP-Core綜合考慮才使用的語法, 由於和Javascript的做用域不一樣, PHP在函數內定義的變量默認就是局部變量, 而在Javascript中則相反,除了顯式定義的纔是局部變量, PHP在變異的時候則沒法肯定變量是局部變量仍是上層做用域內的變量, 固然也可能有辦法在編譯時肯定,不過這樣對於語言的效率和複雜性就有很大的影響。

這個語法比較直接,若是須要訪問上層做用域內的變量則須要使用use語句來申明, 這樣也簡單易讀,說到這裏, 其實可使用use來實現相似global語句的效果。

匿名函數在每次執行的時候都能訪問到上層做用域內的變量, 這些變量在匿名函數被銷燬以前始終保存着本身的狀態,例如以下的例子:

  1. <?php  
  2. function getCounter() {  
  3.     $i = 0;  
  4.     return function() use($i) { // 這裏若是使用引用傳入變量: use(&$i)  
  5.         echo ++$i;  
  6.     };  
  7. }  
  8.    
  9. $counter = getCounter();  
  10. $counter(); // 1  
  11. $counter(); // 1  

 

和Javascript中不一樣,這裏兩次函數調用並無使$i變量自增,默認PHP是經過拷貝的方式傳入上層變量進入匿名函數,若是須要改變上層變量的值則須要經過引用的方式傳遞。因此上面得代碼沒有輸出1, 2而是1,1

閉包的實現

前 面提到匿名函數是經過閉包來實現的, 如今咱們開始看看閉包(類)是怎麼實現的。匿名函數和普通函數除了是否有變量名之外並無區別,閉包的實現代碼在$PHP_SRC/Zend /zend_closure.c。匿名函數"對象化"的問題已經經過Closure實現, 而對於匿名是怎麼樣訪問到建立該匿名函數時的變量的呢?

例如以下這段代碼:

  1. <?php  
  2. $i=100;  
  3. $counter = function() use($i) {  
  4.     debug_zval_dump($i);  
  5. };    
  6.    
  7. $counter();  

 

經過VLD來查看這段編碼編譯什麼樣的opcode了

  1. $ php -dvld.active=1 closure.php  
  2.    
  3. vars:  !0 = $i, !1 = $counter  
  4. # *  op                           fetch          ext  return  operands  
  5. ------------------------------------------------------------------------  
  6. 0  >   ASSIGN                                                   !0, 100  
  7. 1      ZEND_DECLARE_LAMBDA_FUNCTION                             '%00%7Bclosure  
  8. 2      ASSIGN                                                   !1, ~1  
  9. 3      INIT_FCALL_BY_NAME                                       !1  
  10. 4      DO_FCALL_BY_NAME                              0            
  11. 5    > RETURN                                                   1  
  12.    
  13. function name:  {closure}  
  14. number of ops:  5  
  15. compiled vars:  !0 = $i  
  16. line     # *  op                           fetch          ext  return  operands  
  17. --------------------------------------------------------------------------------  
  18.   3     0  >   FETCH_R                      static              $0      'i'  
  19.         1      ASSIGN                                                   !0, $0  
  20.   4     2      SEND_VAR                                                 !0  
  21.         3      DO_FCALL                                      1          'debug_zval_dump'  
  22.   5     4    > RETURN                                                   null  

 

上 面根據狀況去掉了一些無關的輸出, 從上到下, 第1開始將100賦值給!0也就是變量$i, 隨後執行ZEND_DECLARE_LAMBDA_FUNCTION,那咱們去相關的opcode執行函數中看看這裏是怎麼執行的, 這個opcode的處理函數位於$PHP_SRC/Zend/zend_vm_execute.h中:

  1. static int ZEND_FASTCALL  ZEND_DECLARE_LAMBDA_FUNCTION_SPEC_CONST_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)  
  2. {  
  3.     zend_op *opline = EX(opline);  
  4.     zend_function *op_array;  
  5.    
  6.     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  
  7. y) == FAILURE ||  
  8.         op_array->type != ZEND_USER_FUNCTION) {  
  9.         zend_error_noreturn(E_ERROR, "Base lambda function for closure not found");  
  10.     }  
  11.    
  12.     zend_create_closure(&EX_T(opline->result.u.var).tmp_var, op_array TSRMLS_CC);  
  13.    
  14.     ZEND_VM_NEXT_OPCODE();  
  15. }  



該函數調用了zend_create_closure()函數來建立一個閉包對象, 那咱們繼續看看位於$PHP_SRC/Zend/zend_closures.c的zend_create_closure()函數都作了些什麼。

  1. ZEND_API void zend_create_closure(zval *res, zend_function *func TSRMLS_DC)  
  2. {  
  3.     zend_closure *closure;  
  4.    
  5.     object_init_ex(res, zend_ce_closure);  
  6.    
  7.     closure = (zend_closure *)zend_object_store_get_object(res TSRMLS_CC);  
  8.    
  9.     closure->func = *func;  
  10.    
  11.     if (closure->func.type == ZEND_USER_FUNCTION) { // 若是是用戶定義的匿名函數  
  12.         if (closure->func.op_array.static_variables) {  
  13.             HashTable *static_variables = closure->func.op_array.static_variables;  
  14.    
  15.             // 爲函數申請存儲靜態變量的哈希表空間  
  16.             ALLOC_HASHTABLE(closure->func.op_array.static_variables);   
  17.             zend_hash_init(closure->func.op_array.static_variables, zend_hash_num_elements(static_variables), NULL, ZVAL_PTR_DTOR, 0);  
  18.    
  19.             // 循環當前靜態變量列表, 使用zval_copy_static_var方法處理  
  20.             zend_hash_apply_with_arguments(static_variables TSRMLS_CC, (apply_func_args_t)zval_copy_static_var, 1, closure->func.op_array.static_variables);  
  21.         }  
  22.         (*closure->func.op_array.refcount)++;  
  23.     }  
  24.    
  25.     closure->func.common.scope = NULL;  
  26. }  

 

如上段代碼註釋中所說, 繼續看看zval_copy_static_var()函數的實現:

  1. static int zval_copy_static_var(zval **p TSRMLS_DC, int num_args, va_list args, zend_hash_key *key)  
  2. {  
  3.     HashTable *target = va_arg(args, HashTable*);  
  4.     zend_bool is_ref;  
  5.    
  6.     // 只對經過use語句類型的靜態變量進行取值操做, 不然匿名函數體內的靜態變量也會影響到做用域以外的變量  
  7.     if (Z_TYPE_PP(p) & (IS_LEXICAL_VAR|IS_LEXICAL_REF)) {  
  8.         is_ref = Z_TYPE_PP(p) & IS_LEXICAL_REF;  
  9.    
  10.         if (!EG(active_symbol_table)) {  
  11.             zend_rebuild_symbol_table(TSRMLS_C);  
  12.         }  
  13.         // 若是當前做用域內沒有這個變量  
  14.         if (zend_hash_quick_find(EG(active_symbol_table), key->arKey, key->nKeyLength, key->h, (void **) &p) == FAILURE) {  
  15.             if (is_ref) {  
  16.                 zval *tmp;  
  17.    
  18.                 // 若是是引用變量, 則建立一個臨時變量一邊在匿名函數定義以後對該變量進行操做  
  19.                 ALLOC_INIT_ZVAL(tmp);  
  20.                 Z_SET_ISREF_P(tmp);  
  21.                 zend_hash_quick_add(EG(active_symbol_table), key->arKey, key->nKeyLength, key->h, &tmp, sizeof(zval*), (void**)&p);  
  22.             } else {  
  23.                 // 若是不是引用則表示這個變量不存在  
  24.                 p = &EG(uninitialized_zval_ptr);  
  25.                 zend_error(E_NOTICE,"Undefined variable: %s", key->arKey);  
  26.             }  
  27.         } else {  
  28.             // 若是存在這個變量, 則根據是不是引用, 對變量進行引用或者複製  
  29.             if (is_ref) {  
  30.                 SEPARATE_ZVAL_TO_MAKE_IS_REF(p);  
  31.             } else if (Z_ISREF_PP(p)) {  
  32.                 SEPARATE_ZVAL(p);  
  33.             }  
  34.         }  
  35.     }  
  36.     if (zend_hash_quick_add(target, key->arKey, key->nKeyLength, key->h, p, sizeof(zval*), NULL) == SUCCESS) {  
  37.         Z_ADDREF_PP(p);  
  38.     }  
  39.     return ZEND_HASH_APPLY_KEEP;  
  40. }  

 

這個函數做爲一個回調函數傳遞給zend_hash_apply_with_arguments()函數, 每次讀取到hash表中的值以後由這個函數進行處理,而這個函數對全部use語句定義的變量值賦值給這個匿名函數的靜態變量, 這樣匿名函數就能訪問到use的變量了。

 

原文連接:

http://www.php-internals.com/book/?p=chapt04/04-04-anonymous-function

相關文章
相關標籤/搜索