php验证返回值,PHP 趣谈 - 判断返回值是否被使用

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 评论

留下您的评论.