[iefreer] 轉載一篇對PHP閉包語法講解比較深入到位的文章,後續還會轉一篇這些新語法如何巧妙應用的文章。
匿名函數在程式語言中出現的比較早,最早出現在Lisp語言中,隨後很多的程式語言都開始有這個功能了,
目前使用比較廣泛的Javascript以及C#,PHP直到5.3才開始真正支援匿名函數,C++的新標準C++0x也開始支援了。
匿名函數是一類不需要指定標示符,而又可以被呼叫的函數或子例程,匿名函數可以方便的作為參數傳遞給其他函數,最常見應用是作為回調函數。
閉包(Closure)
說到匿名函數,就不得不提到閉包了,閉包是詞法閉包(Lexical Closure)的簡稱,是引用了自由變量的函數,這個被應用的自由變量將和這個函數一同存在,即使離開了創建它的環境也一樣,所以閉包也可認為是有函數和與其相關引用組合而成的實體。在某些語言中,在函數內定義另一個函數的時候,如果內部函數引用到外部函數的變量,則可能產生閉包。在運行外部函數時,一個閉包就形成了。
這個字和匿名函數很容易被混用,其實這是兩個不同的概念,這可能是因為很多語言實作匿名函數的時候允許形成閉包。
使用create_function()建立"匿名"函數
前面提到PHP5.3才開始正式支援匿名函數,說到這裡可能會有細心讀者有意見了,因為有個函數是可以產生匿名函數的: create_function函數,在手冊裡可以查到這個函數在PHP4.1和PHP5中就有了,這個函數通常也能作為匿名回呼函數使用,例如如下:
[php]
觀點 plaincopy
-
-
2, 3, 4); array_walk
- ($array, create_function($array, create_function('$value', 'echo $value'));'echo $value'));當然也能做更多的事情。 那為什麼這不算真正的匿名函數呢,我們先看看這個函數的回傳值,這個函數回傳一個字串,通常我們可以像下面這樣呼叫一個函數:
[php]
view plaincopy
echo
'function a'; } $a
=
();
我們在實現回調函數的時候也可以用這樣的方式,例如:[php]
view plaincopy
) {
// doing
-
- # ...
- // done ;
- }
這樣就能實現於函數do_something ()執行完成之後呼叫$callback指定的函數。回到create_function函數的回傳值:函數傳回一個唯一的字串函數名,出現錯誤的話則回傳FALSE。這麼說這個函數也只是動態的創建了一個函數,而這個函數是有函數名的,也就是說,其實這並不是匿名的。只是創建了一個全域唯一的函數而已。 [php]
view plaincopy
-
$func = create_function( ); echo
- $func; // lambda_1 . // Function created dynamic
-
-
- $my_func = 'lambda_1';
- 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]
view plaincopy
$func ,function( ;');
$my_func_name- = 'lambda_1'; // 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- [php]
view plaincopy
- #define LAMBDA_TEMP_FUNCNAME "__lambda_func" _F
- {
-
// ... 省去無關代碼
- // ... 省去無關代碼
- function_name = (char *) emalloc(sizeof("0lambda_"
)+MAX_LENGTH_OF_LONG); - function_name[0] = ' '
; -
// do
ngth = 1 + sprintf(function_name + 1, - “lambda_%d”, ++EG( lambda_count ));
} - while (zend_hash_add(EG(function_table), function_name
zend_hash_del(EG(function_table), LAMBDA_TEMP_FUNCNAME, sizeof(LAMBDA_TEMP_FUNCNAME)); - RETURN_STRINGL(function_name, function_name_length, 0);
}
- 該函數在定義了一個函數之後,給函數起個名字,將函數名的第一個字符修改為'',然後在函數表中查找是否已經定義了這個函數,如果已經有了則產生新的函數名,第一個字元為空字元的定義方式比較特殊,因為在使用者程式碼中無法定義出這樣的函數,也不會存在命名衝突的問題了,這也是土種取巧(tricky)的做法,在了解了這個特殊的函數之後,我們其實還是可以調用到這個函數的,只要我們在函數名前加一個空字符就可以了,chr()函數可以幫助我們生成這樣的字串,例如前面建立的函數可以透過以下的方式存取:
- [php]
看純文字
- 「lambda_1」
;
-
$my_func(); // Hello 這種創建「匿名函數」的方式有一些缺點:
- 函數的定義是透過字串動態評估的方法,此時無法進行基本檢查的語法; 此類函數和普通函數沒有本質區別,無法實現閉包的效果。引入的__invoke
魔幻方法。符重載一些類似的,例如可以像下面這樣使用:[php]
查看 plaincopy
- class
- Callme
函數
__invoke(
$phone_num
) {
echo- 「你好:$phone_num」
; - }
- } 打電話給我(); $撥打(13810688888);
- // "Hello: 13810688888 函數確實就是用這種方式實現的。
看純文字
- $func = f echo
"Hello, anonymous function"-
; }
echo-
get object- echo get_class($func ); // Closure
-
原來匿名函數也只是一個普通的類別而已。熟悉Javascript的同學對匿名函數的使用方法很熟悉了,PHP也使用和Javascript類似的語法來定義, 匿名函數可以賦值給一個變量, 因為匿名函數其實是一個類別實例, 所以能複製也是很容易理解的, 在Javascript中可以將一個匿名函數賦值給一個物件的屬性, 例如:[php]
view plaincopyvar
a = {};
a.callcom
);}
-
a .call(); // alert called // alert called
- 這在Javascript中使用的導致類別尋找類別中定義的方法,在PHP中屬性名稱和定義的方法名稱是可以重複的, 這是由PHP的類別模型所決定的, 當然PHP在這方面是可以改進的, 後續的版本中可能會允許這樣的調用,這樣的話就更容易靈活的實作一些功能了。目前想要實現這樣的效果也是有方法的: 使用另外一個魔幻方法__call(),至於怎麼實現就留給各位讀者當做習題吧。 閉包的使用
PHP使用閉包(Closure)來實現匿名函數, 匿名函數最強大的功能也就在匿名函數所提供的一些動態特性以及閉包效果,匿名函數在定義的時候如果需要使用作用域外的變數需要使用以下的語法來實現:- [php]
view plaincopy
$name
$func
=
- function() use
- ($name) {
-
} $func (); // Hello TIPI Team 詞
- 語句看起來還是挺起來的比較語法, 因為和Javascript的作用域不同, PHP在函數內定義的變量預設就是局部變量, 而在Javascript中則相反,除了明確定義的才是局部變量, PHP在變異的時候則無法確定變量是局部變數還是上層作用域內的變量, 當然也可能有辦法在編譯時確定,不過這樣對於語言的效率和複雜性就有很大的影響。 這個語法比較直接,如果需要存取上層作用域內的變數則需要使用use語句來申明, 這樣也簡單易讀,說到這裡, 其實可以使用use來實現類似global語句的效果。 匿名函數在每次執行的時候都能訪問到上層作用域內的變量, 這些變量在匿名函數被銷毀之前始終保存著自己的狀態,例如如下的例子:
- [php]
view plaincopy
- function getCounter() {
- return
function- () use($i) { // 這裡如果使用引用傳入變數: use(&$i) ++$i ;
}; - } @ );
- $counter
(); -
// 1
- $counter(); // 1
- 與增加P是透過拷貝的方式傳入上層變數進入匿名函數,如果需要改變上層變數的值則需要以引用的方式傳遞。所以上面得程式碼沒有輸出1, 2而是1,1。 閉包的實作前面提到匿名函數是透過閉包來實現的, 現在我們開始看看閉包(類)是怎麼實現的。匿名函數和普通函數除了是否有變數名稱以外並沒有差別,閉包的實作碼在$PHP_SRC/Zend/zend_closure.c。匿名函數"物件化"的問題已經透過Closure實現, 而對於匿名是怎麼樣存取到創建該匿名函數時的變數的呢?例如如下這段程式碼:
- [php]
view plaincopy
$i=100;
function
() use( $i
) {
- debug_zval_dump($i);
- $counter();
- 透過VLD來查看這段編碼編譯什麼樣的opcode了[php]
view plaincopy
- $ php -dvld.active=1 closure.php
-
- $計數器 # * op 以
----------------------------------- - ------------------------------------------------ - 0 > 分配 !0, 100
1 ZEND_DECLARE_LAMBDA_FUNCTION '%00%7Bclosure 2 分配 !1, ~1 3 INIT_FCALL_BY_NAME !1 4 DO_FCALL_BY_NAME 0 0 -
5 > 回 1
- 函數 名稱: {closure}
- 操作數量: 5
- 編譯變量: !0 = $i
- 行 # * 操作 獲取 ext 返回 操作數
- ---- -------------------------------------------------- --------------------------
-
3 0 > FETCH_R $0 'i' 1 分配 !0,
$0-
4 0
-
3 DO_FCALL 1
'有使用 從上到下,第1開始將100個情況賦值給! 0則表示$i,判斷執行ZEND_DECLARE_LAMB DA_FUNCTION,那我們去相關的操作碼執行函數看看這裡是怎麼執行的,這個操作碼的處理函數位於$PHP_SRC/Zend/zend_vm_execute.h中:- [ php]
檢視純文字
- 靜態
int ZEND_FASTCALL ZEND_DECLARE_LAMBDA_FUNCTION_SPEC_CONST_CONST_OPvER(ZEND__NCODE_FUNCTION
zend_op *opline = EX(opline);
- if (zend_hash_quick_find(EG(function_table), Z_STRVAL(opline->op1.u.constant), Z_STRSTRLENfline-VAL(opline->op1.u.constant), Z_STRLENfL.Lopf. ), (void *) &op_arra
y) == 失敗 || -
-
zend_error_noreturn(E_ERROR,
"未找到用於閉包的基本lambda 函數「- );
-
}
- .u.var).tmp_var, op_array TSRMLS_CC);
-
-
ZEND_VM_NEXT_OPCODE();
-
}
這個函數呼叫了zend_create_closure()函數來建立一個閉包物件, 那我們繼續看看位於$PHP_SRC/Zend/zend_closures.c的zend_create_closure()函數都做了些什麼。
[php]
view plaincopy
- ZEND_API void zend_create_closure(zval *res, zend_function *func TS zend_closure *closure;
-
- object_init_ex(cmure);
-
- closure = (zend_closure *)zend_object_
- closure->func = *func;
-
- ->func.type == ZEND_USER_FUNCTION) { // 如果是使用者定義的匿名函數
-
- 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_Delements;
-
-
// 循環中使用靜態變數項清單,而使用-
zend_hash_apply_with_arguments(static_variables zend_hash_apply_with_arguments(static_variables TSRMLS,_LLLLam集co_pools_pMr. >func.op_array.static_variables);
-
}
- +;
- }
- }
如上段程式碼註解所說, 繼續看看zval_copy_static_var( )函數的實作: [php]
view plaincopy
- static int zval_copy_static_var(zval **p TSRMLS_DC, int num_args, va_list args,
- HashTable *target = va_arg(args, HashTable*);
- zend_bool is_ref;
-
- 版本下對靜態中變數所影響的變數值
-
if (Z_TYPE_PP(p) & (IS_LEXICAL_VAR|IS_LEXICAL_REF))) = Z_TYPE_PP(p) & IS_LEXICAL_REF;
-
(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) {
(is_ref) { - zval * tmp;
-
- ALLOC_INIT_ZVAL(tmp); Z_SET_ISREF_P(tmp);
- zend_hash_quick_add(EG()_symbolboltabletable), sizeof(zval*), (void**)&p);
- } else
- {
p = zend_error(E_NOTICE,- "Undefined variable : %s", key->arKey);
-
{
// 若有這個變數,而 依變數進行引用或複製-
if SEPARATE_ZVAL_TO_MAKE_IS_REF(p); } else if (Z_ISREF_PP(p)) {
-
- }
- if
- ( zend_hash_quick_add(target, key->arKey, key->nKeyLength, key->h, p, sizeof(zval*), NULL) == SUCCESS) { ;
- } return
ZEND_HASH_APPLY_KEEP;
- } ly_with_arguments()函數, 每次讀取hash表中的值之後就由這個函數進行處理,而這個函數對所有use語句定義的變數值賦值給這個匿名函數的靜態變量, 這樣匿名函數就能存取到use的變數了。
原文連結:- http://www.php-internals.com/book/?p=chapt04/04-04-anonymous-function
參考閱讀:http://php.net/manual/zh/functions.anonymous.php
以上就介紹了PHP匿名函數及閉包,包括了方面的內容,希望對PHP教程有興趣的朋友有幫助。