Detailed explanation of anonymous functions and closures in php

怪我咯
Release: 2023-03-11 17:34:02
Original
1166 people have browsed it

Anonymous functions appeared relatively early in Programming languages. They first appeared in Lisp language, and then many programming languages ​​began to have them. This function is available.

Javascript and C# are currently widely used. PHP did not really support anonymous functions until 5.3, and the new C++ standard C++0x also began to support it.

Anonymous function is a type of function or subroutine that can be called without specifying an identifier. Anonymous functions can be conveniently passed as parameters to other functions. The most common application is as a callback function.

Closure

Speaking of anonymous functions, we have to mention closure. Closure is the abbreviation of Lexical Closure, which refers to free variables. Function, the applied free variable will exist with this function even if it leaves the environment in which it was created, so a closure can also be considered as an entity composed of a function and its related references. In some languages, when defining another function within a function, if the internal function references the variables of the external function, a closure may occur. When running an external function, a closure is formed. The word

is easily confused with anonymous functions. In fact, they are two different concepts. This may be because many languages ​​allow closures to be formed when implementing anonymous functions.

Use create_function() to create "anonymous" functions

As mentioned earlier, anonymous functions were only officially supported in PHP5.3. At this point, careful readers may have opinions, because there are A function can generate an anonymous function: create_function function. You can find this function in PHP4.1 and PHP5 in the manual. This function can usually also be used as an anonymous callback function, for example, as follows:

<?php
 
$array = array(1, 2, 3, 4);
array_walk($array, create_function(&#39;$value&#39;, &#39;echo $value&#39;));
Copy after login

This code just outputs the values ​​​​in the array one by one. Of course, it can also do more things. So why is this not a true anonymous function? Let’s first look at the return value of this function. This function returns a string. Usually we can call a function like the following:

<?php
 
function a() {
    echo &#39;function a&#39;;
}
 
$a = &#39;a&#39;;
$a();
Copy after login

We can also use this method when implementing the callback function, for example:

<?php
 
function do_something($callback) {
    // doing
    # ...
 
    // done
    $callback();
}
Copy after login

In this way, the function specified by $callback can be called after the function do_something() is executed. Returning to the return value of the create_function function: the function returns a unique string function name, or FALSE if an error occurs. So this function only dynamically creates a function, and this function has a function name, which means that it is not actually anonymous. It just creates a globally unique function.

<?php
$func = create_function(&#39;&#39;, &#39;echo "Function created dynamic";&#39;);
echo $func; // lambda_1
 
$func();    // Function created dynamic
 
$my_func = &#39;lambda_1&#39;;
$my_func(); // 不存在这个函数
lambda_1(); // 不存在这个函数
Copy after login

The first part of the above code is easy to understand. This is how create_function is used. However, later calling it through the function name fails. This is a bit difficult to understand. How does PHP ensure that this function is globally unique? ? lambda_1 also seems to be a very common function name. What if we first define a function called lambda_1? The return string of the function here will be lambda_2. When creating the function, it will check whether the function exists until it finds the appropriate one. function name, but what if we define a function called lambda_1 after create_function? This will cause the problem of repeated definition of functions. This implementation is probably not the best method. In fact, if you really define the name The function that is lambda_1 will not have the problem I mentioned. What is going on? The last two lines of the above code also illustrate this problem. In fact, there is no function named lambda_1 defined.

That is to say, our lambda_1 and the lambda_1 returned by create_function are not the same!? How could this be the case? That only means that we have not seen the essence, only the surface, and the surface is when we echo Lambda_1 is output, and our lambda_1 is typed in by ourselves. Let's use the debug_zval_dump function to take a look.

<?php
$func = create_function(&#39;&#39;, &#39;echo "Hello";&#39;);
 
$my_func_name = &#39;lambda_1&#39;;
debug_zval_dump($func);         // string(9) "lambda_1" refcount(2)
debug_zval_dump($my_func_name); // string(8) "lambda_1" refcount(2)
Copy after login

You can see it, their lengths are actually different. Different lengths mean that they are not the same function, so of course the function we call does not exist. Let’s just look at the create_function function. What have you done? See this implementation: $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] = &#39;\0&#39;;  // <--- 这里
    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);
}
Copy after login

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

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

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

  1. 函数的定义是通过字符串动态eval的, 这就无法进行基本的语法检查;

  2. 这类函数和普通函数没有本质区别, 无法实现闭包的效果.

真正的匿名函数

在PHP5.3引入的众多功能中, 除了匿名函数还有一个特性值得讲讲: 新引入的invoke 魔幻方法。

invoke魔幻方法

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

<?php
class Callme {
    public function invoke($phone_num) {
        echo "Hello: $phone_num";
    }
}
 
$call = new Callme();
$call(13810688888); // "Hello: 13810688888
Copy after login

匿名函数的实现

前面介绍了将对象作为函数调用的方法, 聪明的你可能想到在PHP实现匿名函数的方法了,PHP中的匿名函数就的确是通过这种方式实现的。我们先来验证一下:

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

原来匿名函数也只是一个普通的类而已。熟悉Javascript的同学对匿名函数的使用方法很熟悉了,PHP也使用和Javascript类似的语法来定义, 匿名函数可以赋值给一个变量, 因为匿名函数其实是一个类实例, 所以能复制也是很容易理解的, 在Javascript中可以将一个匿名函数赋值给一个对象的属性, 例如:

var a = {};
a.call = function() {alert("called");}
a.call(); // alert called
Copy after login

这在Javascript中很常见, 但在PHP中这样并不可以, 给对象的属性复制是不能被调用的, 这样使用将会导致类寻找类中定义的方法,在PHP中属性名和定义的方法名是可以重复的, 这是由PHP的类模型所决定的, 当然PHP在这方面是可以改进的, 后续的版本中可能会允许这样的调用,这样的话就更容易灵活的实现一些功能了。目前想要实现这样的效果也是有方法的: 使用另外一个魔幻方法call(),至于怎么实现就留给各位读者当做习题吧。

闭包的使用

PHP使用闭包(Closure)来实现匿名函数, 匿名函数最强大的功能也就在匿名函数所提供的一些动态特性以及闭包效果,匿名函数在定义的时候如果需要使用作用域外的变量需要使用如下的语法来实现:

<?php
$name = &#39;TIPI Team&#39;;
$func = function() use($name) {
    echo "Hello, $name";
}
 
$func(); // Hello TIPI Team
Copy after login

这个use语句看起来挺别扭的, 尤其是和Javascript比起来, 不过这也应该是PHP-Core综合考虑才使用的语法, 因为和Javascript的作用域不同, PHP在函数内定义的变量默认就是局部变量, 而在Javascript中则相反,除了显式定义的才是局部变量, PHP在变异的时候则无法确定变量是局部变量还是上层作用域内的变量, 当然也可能有办法在编译时确定,不过这样对于语言的效率和复杂性就有很大的影响。

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

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

<?php
function getCounter() {
    $i = 0;
    return function() use($i) { // 这里如果使用引用传入变量: use(&$i)
        echo ++$i;
    };
}
 
$counter = getCounter();
$counter(); // 1
$counter(); // 1
Copy after login

和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();
Copy after login

通过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                             &#39;%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      &#39;i&#39;
        1      ASSIGN                                                   !0, $0
  4     2      SEND_VAR                                                 !0
        3      DO_FCALL                                      1          &#39;debug_zval_dump&#39;
  5     4    > RETURN                                                   null
Copy after login

上面根据情况去掉了一些无关的输出, 从上到下, 第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();
}
Copy after login

该函数调用了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;
}
Copy after login

如上段代码注释中所说, 继续看看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;
}
Copy after login

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

The above is the detailed content of Detailed explanation of anonymous functions and closures in php. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
php
source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template