[iefreer] 转载一篇对

匿名函数在编程语言中出现的比较早,最早出现在Lisp语言中,随后很多的编程语言都开始有这个功能了,

目前使用比较广泛的Javascript以及C#,

匿名函数是一类不需要指定标示符,而又可以被调用的函数或子例程,匿名函数可以方便的作为参数传递给其他函数,最常见应用是作为回调函数。

闭包(Closure)

说到匿名函数,就不得不提到闭包了,闭包是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数,这个被应用的自由变量将和这个函数一同存在,即使离开了创建它的环境也一样,所以闭包也可认为是有函数和与其相关引用组合而成的实体。在一些语言中,在函数内定义另一个函数的时候,如果内部函数引用到外部函数的变量,则可能产生闭包。在运行外部函数时,一个闭包就形成了。

这个词和匿名函数很容易被混用,其实这是两个不同的概念,这可能是因为很多语言实现匿名函数的时候允许形成闭包。

使用create_function()创建"匿名"函数

前面提到

[

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


这段代码只是将数组中的值依次输出,当然也能做更多的事情。 那为什么这不算真正的匿名函数呢,我们先看看这个函数的返回值,这个函数返回一个字符串,通常我们可以像下面这样调用一个函数:

[

  1. function a() {
  2. echo 'function a';
  3. }
  4. $a = 'a';
  5. $a();


我们在实现回调函数的时候也可以采用这样的方式,例如:

[

  1. function do_something($callback) {
  2. // doing
  3. # ...
  4. // done
  5. $callback();
  6. }


这样就能实现在函数do_something()执行完成之后调用$callback指定的函数。回到create_function函数的返回值:函数返回一个唯一的字符串函数名,出现错误的话则返回FALSE。这么说这个函数也只是动态的创建了一个函数,而这个函数是有函数名的,也就是说,其实这并不是匿名的。只是创建了一个全局唯一的函数而已。

[

  1. $func = create_function('', 'echo "Function created dynamic";');
  2. echo $func; // lambda_1
  3. $func(); // Function created dynamic
  4. $my_func = 'lambda_1';
  5. $my_func(); // 不存在这个函数
  6. lambda_1(); // 不存在这个函数


上面这段代码的前面很好理解,create_function就是这么用的,后面通过函数名来调用却失败了,这就有些不好理解了,

也就是说我们的lambda_1和create_function返回的lambda_1并不是一样的!? 怎么会这样呢? 那只能说明我们没有看到实质,只看到了表面,表面是我们在echo的时候输出了lambda_1,而我们的lambda_1是我们自己敲入的. 我们还是使用debug_zval_dump函数来看看吧。

[

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


看出来了吧,他们的长度居然不一样,长度不一样也即是说不是同一个函数,所以我们调用的函数当然是不存在的,我们还是直接看看create_function函数到底都做了些什么吧。该实现见: $

[

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


该函数在定义了一个函数之后,给函数起了个名字,它将函数名的第一个字符变为了'\0'也就是空字符,然后在函数表中查找是否已经定义了这个函数,如果已经有了则生成新的函数名, 第一个字符为空字符的定义方式比较特殊, 因为在用户代码中无法定义出这样的函数, 也就不存在命名冲突的问题了,这也算是种取巧(tricky)的做法,在了解到这个特殊的函数之后,我们其实还是可以调用到这个函数的, 只要我们在函数名前加一个空字符就可以了, chr()函数可以帮我们生成这样的字符串, 例如前面创建的函数可以通过如下的方式访问到:

[

  1. $my_func = chr(0) . "lambda_1";
  2. $my_func(); // Hello


这种创建"匿名函数"的方式有一些缺点:

  1. 函数的定义是通过字符串动态eval的, 这就无法进行基本的语法检查;
  2. 这类函数和普通函数没有本质区别, 无法实现闭包的效果.

真正的匿名函数

__invoke魔幻方法

这个魔幻方法被调用的时机是: 当一个对象当做函数调用的时候, 如果对象定义了__invoke魔幻方法则这个函数会被调用,这和C++中的操作符重载有些类似, 例如可以像下面这样使用:

[

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


匿名函数的实现

前面介绍了将对象作为函数调用的方法, 聪明的你可能想到在

[

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


原来匿名函数也只是一个普通的类而已。熟悉Javascript的同学对匿名函数的使用方法很熟悉了,

[

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


这在Javascript中很常见, 但在

闭包的使用

[

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


这个use语句看起来挺别扭的, 尤其是和Javascript比起来, 不过这也应该是

这个语法比较直接,如果需要访问上层作用域内的变量则需要使用use语句来申明, 这样也简单易读,说到这里, 其实可以使用use来实现类似global语句的效果。

匿名函数在每次执行的时候都能访问到上层作用域内的变量, 这些变量在匿名函数被销毁之前始终保存着自己的状态,例如如下的例子:

[

  1. function getCounter() {
  2. $i = 0;
  3. return function() use($i) { // 这里如果使用引用传入变量: use(&$i)
  4. echo ++$i;
  5. };
  6. }
  7. $counter = getCounter();
  8. $counter(); // 1
  9. $counter(); // 1


和Javascript中不同,这里两次函数调用并没有使$i变量自增,默认1, 2而是1,1

闭包的实现

前面提到匿名函数是通过闭包来实现的, 现在我们开始看看闭包(类)是怎么实现的。匿名函数和普通函数除了是否有变量名以外并没有区别,闭包的实现代码在$

例如如下这段代码:

[

  1. $i=100;
  2. $counter = function() use($i) {
  3. debug_zval_dump($i);
  4. };
  5. $counter();


通过VLD来查看这段编码编译什么样的opcode了

[

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


上面根据情况去掉了一些无关的输出, 从上到下, 第1开始将100赋值给!0也就是变量$i, 随后执行ZEND_DECLARE_LAMBDA_FUNCTION,那我们去相关的opcode执行函数中看看这里是怎么执行的, 这个opcode的处理函数位于$

[

  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. 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
  6. y) == FAILURE ||
  7. op_array->type != ZEND_USER_FUNCTION) {
  8. zend_error_noreturn(E_ERROR, "Base lambda function for closure not found");
  9. }
  10. zend_create_closure(&EX_T(opline->result.u.var).tmp_var, op_array TSRMLS_CC);
  11. ZEND_VM_NEXT_OPCODE();
  12. }


该函数调用了zend_create_closure()函数来创建一个闭包对象, 那我们继续看看位于$

[

  1. ZEND_API void zend_create_closure(zval *res, zend_function *func TSRMLS_DC)
  2. {
  3. zend_closure *closure;
  4. object_init_ex(res, zend_ce_closure);
  5. closure = (zend_closure *)zend_object_store_get_object(res TSRMLS_CC);
  6. closure->func = *func;
  7. if (closure->func.type == ZEND_USER_FUNCTION) { // 如果是用户定义的匿名函数
  8. if (closure->func.op_array.static_variables) {
  9. HashTable *static_variables = closure->func.op_array.static_variables;
  10. // 为函数申请存储静态变量的哈希表空间
  11. ALLOC_HASHTABLE(closure->func.op_array.static_variables);
  12. zend_hash_init(closure->func.op_array.static_variables, zend_hash_num_elements(static_variables), NULL, ZVAL_PTR_DTOR, 0);
  13. // 循环当前静态变量列表, 使用zval_copy_static_var方法处理
  14. zend_hash_apply_with_arguments(static_variables TSRMLS_CC, (apply_func_args_t)zval_copy_static_var, 1, closure->func.op_array.static_variables);
  15. }
  16. (*closure->func.op_array.refcount)++;
  17. }
  18. closure->func.common.scope = NULL;
  19. }


如上段代码注释中所说, 继续看看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. // 只对通过use语句类型的静态变量进行取值操作, 否则匿名函数体内的静态变量也会影响到作用域之外的变量
  6. if (Z_TYPE_PP(p) & (IS_LEXICAL_VAR|IS_LEXICAL_REF)) {
  7. is_ref = Z_TYPE_PP(p) & IS_LEXICAL_REF;
  8. if (!EG(active_symbol_table)) {
  9. zend_rebuild_symbol_table(TSRMLS_C);
  10. }
  11. // 如果当前作用域内没有这个变量
  12. if (zend_hash_quick_find(EG(active_symbol_table), key->arKey, key->nKeyLength, key->h, (void **) &p) == FAILURE) {
  13. if (is_ref) {
  14. zval *tmp;
  15. // 如果是引用变量, 则创建一个临时变量一边在匿名函数定义之后对该变量进行操作
  16. ALLOC_INIT_ZVAL(tmp);
  17. Z_SET_ISREF_P(tmp);
  18. zend_hash_quick_add(EG(active_symbol_table), key->arKey, key->nKeyLength, key->h, &tmp, sizeof(zval*), (void**)&p);
  19. } else {
  20. // 如果不是引用则表示这个变量不存在
  21. p = &EG(uninitialized_zval_ptr);
  22. zend_error(E_NOTICE,"Undefined variable: %s", key->arKey);
  23. }
  24. } else {
  25. // 如果存在这个变量, 则根据是否是引用, 对变量进行引用或者复制
  26. if (is_ref) {
  27. SEPARATE_ZVAL_TO_MAKE_IS_REF(p);
  28. } else if (Z_ISREF_PP(p)) {
  29. SEPARATE_ZVAL(p);
  30. }
  31. }
  32. }
  33. if (zend_hash_quick_add(target, key->arKey, key->nKeyLength, key->h, p, sizeof(zval*), NULL) == SUCCESS) {
  34. Z_ADDREF_PP(p);
  35. }
  36. return ZEND_HASH_APPLY_KEEP;
  37. }


这个函数作为一个回调函数传递给zend_hash_apply_with_arguments()函数, 每次读取到hash表中的值之后由这个函数进行处理,而这个函数对所有use语句定义的变量值赋值给这个匿名函数的静态变量, 这样匿名函数就能访问到use的变量了。

原文链接:

http://www.参考阅读:

http:// 以上就介绍了
09-09 22:17