詳解php 中的匿名函數與閉包

來源:互聯網
上載者:User
匿名函數在程式設計語言中出現的比較早,最早出現在Lisp語言中,隨後很多的程式設計語言都開始有這個功能了,

目前使用比較廣泛的Javascript以及C#,PHP直到5.3才開始真正支援匿名函數,C++的新標準C++0x也開始支援了。

匿名函數是一類不需要指定標示符,而又可以被調用的函數或子常式,匿名函數可以方便的作為參數傳遞給其他函數,最常見應用是作為回呼函數。

閉包(Closure)

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

這個詞和匿名函數很容易被混用,其實這是兩個不同的概念,這可能是因為很多語言實現匿名函數的時候允許形成閉包。

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

前面提到PHP5.3中才才開始正式支援匿名函數,說到這裡可能會有細心讀者有意見了,因為有個函數是可以產生匿名函數的: 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函數來看看吧。

<?php$func = create_function('', 'echo "Hello";'); $my_func_name = 'lambda_1';debug_zval_dump($func);         // string(9) "lambda_1" refcount(2)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'也就是Null 字元,然後在函數表中尋找是否已經定義了這個函數,如果已經有了則產生新的函數名, 第一個字元為空白字元的定義方式比較特殊, 因為在使用者代碼中無法定義出這樣的函數, 也就不存在命名衝突的問題了,這也算是種取巧(tricky)的做法,在瞭解到這個特殊的函數之後,我們其實還是可以調用到這個函數的, 只要我們在函數名前加一個Null 字元就可以了, chr()函數可以幫我們產生這樣的字串, 例如前面建立的函數可以通過如下的方式訪問到:

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

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

  1. 函數的定義是通過字串動態eval的, 這就無法進行基本的語法檢查;

  2. 這類函數和普通函數沒有本質區別, 無法實現閉包的效果.

真正的匿名函數

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

invoke魔幻方法

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

<?phpclass Callme {    public function invoke($phone_num) {        echo "Hello: $phone_num";    }} $call = new Callme();$call(13810688888); // "Hello: 13810688888

匿名函數的實現

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

<?php$func = function() {    echo "Hello, anonymous function";} echo gettype($func);    // objectecho get_class($func);  // Closure

原來匿名函數也只是一個普通的類而已。熟悉Javascript的同學對匿名函數的使用方法很熟悉了,PHP也使用和Javascript類似的文法來定義, 匿名函數可以賦值給一個變數, 因為匿名函數其實是一個類執行個體, 所以能複製也是很容易理解的, 在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 "Hello, $name";} $func(); // Hello TIPI Team

這個use語句看起來挺彆扭的, 尤其是和Javascript比起來, 不過這也應該是PHP-Core綜合考慮才使用的文法, 因為和Javascript的範圍不同, PHP在函數內定義的變數預設就是局部變數, 而在Javascript中則相反,除了顯式定義的才是局部變數, PHP在變異的時候則無法確定變數是局部變數還是上層範圍內的變數, 當然也可能有辦法在編譯時間確定,不過這樣對於語言的效率和複雜性就有很大的影響。

這個文法比較直接,如果需要訪問上層範圍內的變數則需要使用use語句來申明, 這樣也簡單易讀,說到這裡, 其實可以使用use來實作類別似global語句的效果。

匿名函數在每次執行的時候都能訪問到上層範圍內的變數, 這些變數在匿名函數被銷毀之前始終儲存著自己的狀態,例如如下的例子:

<?phpfunction getCounter() {    $i = 0;    return function() use($i) { // 這裡如果使用引用傳入變數: use(&$i)        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, 1001      ZEND_DECLARE_LAMBDA_FUNCTION                             '%00%7Bclosure2      ASSIGN                                                   !1, ~13      INIT_FCALL_BY_NAME                                       !14      DO_FCALL_BY_NAME                              0          5    > RETURN                                                   1 function name:  {closure}number of ops:  5compiled vars:  !0 = $iline     # *  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_array) == 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的變數了。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.