PHP匿名函数:公开黑魔法会议



可能应该从以下事实开始:PHP中的匿名函数(闭包)不是函数,而是Closure类的对象。 实际上,本文可能已经完成,但是如果有人对这些细节感兴趣,欢迎关注。


为了没有根据:
$func = function (){}; var_dump($func); --------- object(Closure)#1 (0) { } 

展望未来,我会说这并不是一个普通的对象。 让我们弄清楚。

例如,这样的代码
 $func = function (){ echo 'Hello world!'; }; $func(); 

编译成这样的一组操作码:
 line #* EIO op fetch ext return operands -------------------------------------------------------------------------- 8 0 E > DECLARE_LAMBDA_FUNCTION '%00%7Bclosure%7D%2Fin%2FcrvX50x7fabda9ed09e' 10 1 ASSIGN !0, ~1 11 2 INIT_DYNAMIC_CALL !0 3 DO_FCALL 0 11 2 > RETURN 1 Function %00%7Bclosure%7D%2Fin%2FcrvX50x7fabda9ed09e: function name: {closure} line #* EIO op fetch ext return operands -------------------------------------------------------------------------- 9 0 E > ECHO 'Hello+world%21' 10 1 > RETURN null 

带有函数体描述的代码块对我们来说并不是特别有趣,但是在第一个代码块中,有两个有趣的操作码: DECLARE_LAMBDA_FUNCTIONINIT_DYNAMIC_CALL 。 让我们从第二个开始。

INIT_DYNAMIC_CALL


当编译器看到变量或数组上的函数调用时,将使用此操作码。 即
 $variable(); ['ClassName', 'staticMethod'](); 

这不是一些特定于闭包的唯一操作码。 通过调用__invoke()方法,此语法也可用于对象,对于包含函数名( $ a ='funcName'; $ a(); )的字符串变量,以及在其中包含类名和静态方法的数组,此语法也适用。

在闭包的情况下,我们感兴趣的是使用逻辑上的对象调用变量。
深入研究处理此操作码的VM代码,我们进入zend_init_dynamic_call_object函数,在其中我们将看到以下内容(切片):
 zend_execute_data *zend_init_dynamic_call_object(zend_object *function, uint32_t num_args) { zend_function *fbc; zend_class_entry *called_scope; zend_object *object; ... if (EXPECTED(function->handlers->get_closure) && EXPECTED(function->handlers->get_closure(function, &called_scope, &fbc, &object) == SUCCESS)) { ... } else { zend_throw_error(NULL, "Function name must be a string"); return NULL; } ... } 

有趣的是,就VM而言,熟悉的__invoke方法调用是尝试调用Closure- get_closure的尝试。

实际上,在这一点上,区别开始于处理对匿名函数的调用和常规对象的__invoke方法。
在PHP中,每个对象都有一组定义其实用程序和magic方法的不同处理程序。
标准集看起来像这样
 ZEND_API const zend_object_handlers std_object_handlers = { 0, /* offset */ zend_object_std_dtor, /* free_obj */ zend_objects_destroy_object, /* dtor_obj */ zend_objects_clone_obj, /* clone_obj */ zend_std_read_property, /* read_property */ zend_std_write_property, /* write_property */ zend_std_read_dimension, /* read_dimension */ zend_std_write_dimension, /* write_dimension */ zend_std_get_property_ptr_ptr, /* get_property_ptr_ptr */ NULL, /* get */ NULL, /* set */ zend_std_has_property, /* has_property */ zend_std_unset_property, /* unset_property */ zend_std_has_dimension, /* has_dimension */ zend_std_unset_dimension, /* unset_dimension */ zend_std_get_properties, /* get_properties */ zend_std_get_method, /* get_method */ zend_std_get_constructor, /* get_constructor */ zend_std_get_class_name, /* get_class_name */ zend_std_compare_objects, /* compare_objects */ zend_std_cast_object_tostring, /* cast_object */ NULL, /* count_elements */ zend_std_get_debug_info, /* get_debug_info */ /* ------- */ zend_std_get_closure, /* get_closure */ /* ------- */ zend_std_get_gc, /* get_gc */ NULL, /* do_operation */ NULL, /* compare */ NULL, /* get_properties_for */ }; 


现在,我们对get_closure处理程序感兴趣。 对于常规对象,它指向zend_std_get_closure函数,该函数检查是否为该对象定义了__invoke函数,并返回指向该对象的指针或错误。 但是对于实现匿名函数的Closure类,在此处理程序数组中,几乎所有实用程序函数都被重新定义,包括那些控制生命周期的实用程序函数。 即 虽然对于用户来说,它看起来像一个普通的对象,但实际上,它是具有超能力的变体:)
为类Closure的对象注册处理程序
 void zend_register_closure_ce(void) /* {{{ */ { zend_class_entry ce; INIT_CLASS_ENTRY(ce, "Closure", closure_functions); zend_ce_closure = zend_register_internal_class(&ce); zend_ce_closure->ce_flags |= ZEND_ACC_FINAL; zend_ce_closure->create_object = zend_closure_new; zend_ce_closure->serialize = zend_class_serialize_deny; zend_ce_closure->unserialize = zend_class_unserialize_deny; memcpy(&closure_handlers, &std_object_handlers, sizeof(zend_object_handlers)); closure_handlers.free_obj = zend_closure_free_storage; closure_handlers.get_constructor = zend_closure_get_constructor; closure_handlers.get_method = zend_closure_get_method; closure_handlers.write_property = zend_closure_write_property; closure_handlers.read_property = zend_closure_read_property; closure_handlers.get_property_ptr_ptr = zend_closure_get_property_ptr_ptr; closure_handlers.has_property = zend_closure_has_property; closure_handlers.unset_property = zend_closure_unset_property; closure_handlers.compare_objects = zend_closure_compare_objects; closure_handlers.clone_obj = zend_closure_clone; closure_handlers.get_debug_info = zend_closure_get_debug_info; /* ------- */ closure_handlers.get_closure = zend_closure_get_closure; /* ------- */ closure_handlers.get_gc = zend_closure_get_gc; } 


该手册说:
除了此处描述的方法外,此类还具有__invoke方法。 仅在与实现魔术调用的其他类兼容时才需要此方法,因为在调用函数时不使用此方法。

这是真的。 闭包的get_closure函数不会返回__invoke ,而是从中创建闭包的函数。

您可以自己更详细地研究资源-文件zend_closure.c ,我们将继续进行下一个操作码。

DECLARE_LAMBDA_FUNCTION


但这是专门用于电路的操作码,不再与任何东西一起使用。 在处理器的内部,有三个主要操作:
  1. 寻找指向已编译函数的指针,这将是闭包的本质。
  2. 定义了创建闭包的上下文(换句话说, )。
  3. 基于前两点, 创建了Closure类的对象。


在这个地方,并不是一个令人愉快的消息开始。

那么匿名函数怎么了?


创建闭包比创建普通对象要困难得多。 创建对象的标准机制不仅被称为,而且还添加了一定数量的逻辑,其中最不愉快的是将函数的整个操作码数组复制到闭包的主体中。 它本身并没有那么可怕,但是直到您开始“错误地”使用它时才如此。

为了确切地了解问题在哪里等待,我们将分析创建闭包时的情况。
重新创建关闭:
a)在DECLARE_LAMBDA_FUNCTION 操作码的每次处理中。
直观地-闭包看起来很好的情况就是这样,但实际上在每次循环迭代时都会创建一个新的闭包对象。
 foreach($values as $value){ doSomeStuff($value, function($args) { closureBody }); } 

b)每次调用 bindbindTo方法时:
在这里,每次迭代也将重新创建闭包。
 $closure = function($args) { closureBody }; foreach($objects as $object){ $closure->bindTo($object); $object->doSomeStuff($closure); } 

c)每次调用调用方法时(如果将生成器用作函数)。 如果不是生成器,而是普通函数,则仅执行复制操作码数组的部分。 这样的事情。

结论


如果性能对您不重要,不惜一切代价,那么匿名功能将很方便且令人愉快。 如果重要,那么可能不值得。

无论如何,现在您知道,如果未正确准备闭包和循环,它们就是这样的组合。

感谢您的关注!

Source: https://habr.com/ru/post/zh-CN478596/


All Articles