>  기사  >  백엔드 개발  >  PHP의 익명 함수 및 클로저에 대한 자세한 설명

PHP의 익명 함수 및 클로저에 대한 자세한 설명

怪我咯
怪我咯원래의
2017-06-28 11:14:421156검색

익명 함수프로그래밍 언어에서 처음 등장했습니다. 이후에는 많은 프로그래밍 언어에서 이 기능을 사용하기 시작했습니다.

현재는 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(&#39;$value&#39;, &#39;echo $value&#39;));

이 코드는 값을 추가합니다. 물론 배열의 순서대로 출력하면 더 많은 일을 할 수 있습니다. 그렇다면 이것이 진정한 익명 함수가 아닌 이유는 무엇입니까? 먼저 이 함수의 반환 값을 살펴보겠습니다. 이 함수는 일반적으로 다음과 같은 함수를 호출할 수 있습니다.

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

콜백 함수를 구현할 때 또한 이 방법을 사용하세요. 예를 들면:

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

이렇게 하면 do_something() 함수가 완료된 후 $callback으로 지정된 함수를 호출할 수 있습니다. create_function 함수의 반환 값으로 돌아가기: 함수는 고유한 문자열 함수 이름을 반환하거나 오류가 발생하면 FALSE를 반환합니다. 그래서 이 함수는 동적으로 함수를 생성할 뿐이고, 이 함수는 함수 이름을 가지고 있습니다. 즉, 실제로는 익명이 아닙니다. 단지 전역적으로 고유한 기능을 생성하는 것뿐입니다.

<?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(); // 不存在这个函数
위 코드의 첫 번째 부분은 create_function이 사용되는 방식입니다. 그러나 나중에 함수 이름을 통해 호출하면 실패하므로 PHP는 이 함수가 전역적으로 사용되는지 어떻게 확인합니까? 이것은 매우 일반적인 함수 이름이기도 합니다. 여기서 함수의 반환 문자열은 함수를 찾을 때까지 함수가 존재하는지 확인합니다. 그러나 create_function 다음에 Lambda_1이라는 함수를 정의하면 어떻게 될까요? 이렇게 하면 함수를 반복적으로 정의하는 문제가 발생할 수 있습니다. 실제로 실제로 Lambda_1이라는 함수를 정의하는 경우에는 이 구현이 최선의 방법이 아닐 것입니다. 언급은 발생하지 않습니다. 무슨 일이 일어나고 있는 걸까요? 위 코드의 마지막 두 줄도 이 문제를 보여줍니다. 실제로 정의된 Lambda_1이라는 함수가 없습니다.

즉, create_function에서 반환된 Lambda_1이 동일하지 않다는 뜻입니다! 어떻게 이런 일이 일어날 수 있을까요? 이는 단지 본질을 보지 못했다는 뜻일 뿐, 표면만 보고 있다는 뜻입니다. 그리고 우리가 직접 입력한 것은 debug_zval_dump 함수입니다.

<?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)

아시다시피, 길이가 다르다는 것은 동일한 함수가 아니라는 것을 의미하므로 당연히 우리가 호출하는 함수는 존재하지 않습니다. 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] = &#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);
}

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

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

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

  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

匿名函数的实现

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

<?php
$func = function() {
    echo "Hello, anonymous function";
}
 
echo gettype($func);    // object
echo 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 = &#39;TIPI Team&#39;;
$func = function() use($name) {
    echo "Hello, $name";
}
 
$func(); // Hello TIPI Team

这个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

和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, 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

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

该函数调用了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的变量了。

위 내용은 PHP의 익명 함수 및 클로저에 대한 자세한 설명의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.