1. 判断返回值的使用情况
当我们调用一个PHP函数时,它可能会返回一个值。然而,并非所有情况下我们都会使用这个值。如果我们不使用返回值,那么这个函数为了返回值所做的各种工作是对CPU资源的浪费。在某些情况下,我们也许想要判断返回值是否被使用。
不幸的是,在PHP中你无法做到这一点,可是如果你是在开发一个PHP扩展,这就变得非常容易了。在zend.h中有一个宏:
#define USED_RET() \
(!EX(prev_execute_data) || \
!ZEND_USER_CODE(EX(prev_execute_data)->func->common.type) || \
(EX(prev_execute_data)->opline->result_type != IS_UNUSED))
我们可以看到,当以下条件至少有一个达成时,可以认为返回值是被使用的。
没有先前的执行数据(execute data)
调用方函数不是在PHP中被定义的
在调用域的当前字节码行中存储的结果类型不是IS_UNUSED
1.1 没有先前的执行数据?
在全局域中,没有先前的执行数据。虽然没有在任何函数或方法中,你仍然可以在全局域中return。如果你这么做,那么其他引用该PHP脚本的其他脚本就可以获得这一返回值。
// foo.php
return 'foo';
// bar.php
$bar = include 'foo.php';
在全局域中宏USED_RET()的值永远为真,但我们不需要考虑这一情况,因为我们在PHP扩展中实现的函数内部不属于全局域。
1.2 不是在PHP中定义?
先前的执行数据的func成员存储了指向调用方函数的指针。现在我们在PHP扩展中实现下面两个函数:
PHP_FUNCTION(foo)
{
RETVAL_LONG(EX(prev_execute_data)->func->common.type);
}
PHP_FUNCTION(bar)
{
zval retval, foo;
ZVAL_STRING(&foo, "foo");
call_user_function(EX(function_table), NULL, &foo, &retval, 0, NULL);
zval_ptr_dtor(&foo);
RETVAL_LONG(Z_LVAL(retval));
}
然后执行以下脚本,你会得到"2 4 1"的输出。
$type_a = foo();
$type_b = eval('return foo();');
$type_c = bar();
echo "$type_a $type_b $type_c", PHP_EOL;
#define ZEND_INTERNAL_FUNCTION 1
#define ZEND_USER_FUNCTION 2
#define ZEND_OVERLOADED_FUNCTION 3
#define ZEND_EVAL_CODE 4
#define ZEND_OVERLOADED_FUNCTION_TEMPORARY 5
/* A quick check (type == ZEND_USER_FUNCTION || type == ZEND_EVAL_CODE) */
#define ZEND_USER_CODE(type) ((type & 1) == 0)
现在一切都很明了,若调用方函数是在PHP脚本中定义的,则函数类型为ZEND_USER_FUNCTION;如果是在eval()中定义的,则为ZEND_EVAL_CODE;如果是在底层实现的原生函数,则为ZEND_INTERNAL_FUNTION。如果是前两者,我们就可以认为这个函数是在PHP中定义的。通过宏ZEND_USER_CODE()可以便捷地进行判断。
如果调用方函数是原生函数,则宏USED_RET()的值永远为真。这是因为你无法判断一个原生函数(如上例中bar())是否使用了返回值。
1.3 字节码行的结果类型?
最终,我们通过判断字节码行(opline)的结果类型来判断返回值是否被调用方使用。你可以将opline理解为"line of opcode",在这里即为调用方正在执行的当前行字节码。
字节码行的result_type可以是下列值之一:
#define IS_CONST (1<<0)
#define IS_TMP_VAR (1<<1)
#define IS_VAR (1<<2)
#define IS_UNUSED (1<<3) /* Unused variable */
#define IS_CV (1<<4) /* Compiled variable */
如果result_type的值为IS_UNUSED,我们就可以认为调用方没有使用当前函数的返回值。
2. 用途
正如我们之前所说,当我们定义的函数的返回值不常被使用,且返回值的过程开销较大,我们就可以使用宏USED_RET()来提升性能。一些PHP的内置函数已经在利用这一功能了。比如一些操作数组内部指针的函数,像next()、prev()、end()、reset()。当用户只需要改变内部指针,而不需要获取当前指针指向的值是,相关函数将会省去取出并返回值的操作。
/* {{{ proto mixed next(array array_arg)
Move array argument's internal pointer to the next element and return it */
PHP_FUNCTION(next)
{
HashTable *array;
zval *entry;
ZEND_PARSE_PARAMETERS_START(1, 1)
Z_PARAM_ARRAY_OR_OBJECT_HT_EX(array, 0, 1)
ZEND_PARSE_PARAMETERS_END();
zend_hash_move_forward(array);
if (USED_RET()) {
if ((entry = zend_hash_get_current_data(array)) == NULL) {
RETURN_FALSE;
}
if (Z_TYPE_P(entry) == IS_INDIRECT) {
entry = Z_INDIRECT_P(entry);
}
ZVAL_DEREF(entry);
ZVAL_COPY(return_value, entry);
}
}
/* }}} */
在所有版本的PHP7中这个宏都是可用的,如果有需要,你可以自由地在PHP扩展中使用它。
3. 如何在PHP脚本中判断?
我们只需要在PHP扩展中实现一个简单的函数,就可以在PHP脚本中判断返回值是否被使用。如下:
PHP_FUNTION(used_ret)
{
zend_execute_data* ex = EX(prev_execute_data);
if (!ex)
RETURN_TRUE;
// 获取先前的执行数据的先前执行数据
ex = ex->prev_execute_data;
if (!ex || !ZEND_USER_CODE(ex->func->common.type))
RETURN_TRUE;
RETURN_BOOL(ex->opline->result_type != IS_UNUSED);
}
现在我们可以写一个脚本测试一下:
function foo()
{
if (used_ret()) {
echo 'Used.', PHP_EOL;
} else {
echo 'Not used.', PHP_EOL;
}
return 0;
}
foo();
$bar = foo();
期望的输出:
Not used.
Used.
本文链接:https://my.lmcjl.com/post/18573.html
4 评论