воскресенье, 23 марта 2014 г.

Коллега, будь аккуратен

Передача произвольного числа параметров в функцию

Есть в PHP удобная, на первый взгляд, возможность - функции с переменным числом параметров. При вызове такой функции ей в качестве параметров через запятую передаётся произвольное количество значений. При этом внутренняя логика вызываемой функции содержит специальный код, который позволяет определить, сколько параметров ей передали, и что именно в них содержится. Делается это совсем не сложно с помощью стандартной библиотеки PHP.

Вот, например, функция, которая все передаваемые ей параметры выводит в столбик на экран.

function printVars() {
    $argsCount = func_num_args();
    $argsArray = func_get_args();
    for ($i = 0; $i < $argsCount; $i++) {
        echo $argsArray[$i] . "\n";
    }
} 

printVars('value 1', 'value 2', 'value 3');

Результат:

   value 1
   value 2
   value 3

Всю специальную логику обеспечивают два обращения к стандартной библиотеке: func_num_args для определения количества переданных параметров и func_get_args для доступа к этим параметрам.

Всё в этом коде хорошо, пока параметры мы передаём явно, но давайте представим такую, достаточно стандартную, ситуацию. Мы получили из базы данных какую-то запись, преобразовали её в одномерный массив и хотим передать элементы массива в функцию printVars. Сколько будет элементов массива, мы заранее не знаем. Вопрос на засыпку - как вызвать нашу функцию?

$valuesArray = array('value 1', 'value 2', 'value 3');
printVars(/* ??? */);

И вот тут, похоже, что-то в PHP недопроектировали. Нет решения данной задачи, которое бы позволило программному коду остаться наглядным и понятным. Первое, что приходит в голову, eval.

$quotesValuesArray = array_map(
    function ($item) { return "'$item'"; }, 
    $valuesArray);
eval("printVars(" . implode(', ', $quotesValuesArray) . ");");

Вариант получается неудобный и может стать источником непредсказуемых трудноуловимых ошибок. Этим, в принципе, страдает любой код, вызываемый через eval. Ужас, ужас!

Интереснее выглядит использование функции call_user_func_array. Она умеет вызывать некую пользовательскую функцию и передавать ей в качестве параметров значения из массива.

call_user_func_array("printVars", $valuesArray);

Код выглядит не идеально, но явно нагляднее, чем первый вариант. Да и мест для потенциальных ошибок стало меньше.

Если мы написали не просто функцию с переменным числом параметров, а метод класса, то вызов call_user_func_array меняется совсем незначительно.

class Example {
    public function printVars() {
        // ...
    }
}
// ...
$example = new Example();
call_user_func_array(array($example, "printVars"), $valuesArray);

Проведём ещё один эксперимент. Изменим функцию printVars, чтобы она изменяла и возвращала передаваемые ей параметры.

function printVars() {
    $argsArray = func_get_args();

    if (is_array($argsArray)) {
        foreach ($argsArray as $key => $arg) {
            $argsArray[$key] = $arg . '!';
        }
    }
}

$value1 = 'value 1';
$value2 = 'value 2';
$value3 = 'value 3';
$valuesArray = array(&$value1, &$value2, &$value3);

call_user_func_array("printVars", $valuesArray);

var_dump($valuesArray);

Получаем:

array(3) {
[0]=>
&string(7) "value 1"
[1]=>
&string(7) "value 2"
[2]=>
&string(7) "value 3"
}

Хм. Но это же совсем не то, что мы хотели получить! Параметры $value1, $value2 и $value3 не изменились. Оказывается, с помощью func_get_args нельзя получить ссылки на исходные параметры функции. Решается эта проблема с помощью злого хака.

function printVars() {
    $trace = debug_backtrace();
    $argsArray = $trace[1]['args'][1];

    if (is_array($argsArray)) {
        foreach ($argsArray as $key => $arg) {
            $argsArray[$key] = $arg . '!';
        }
    }

}

$value1 = 'value 1';
$value2 = 'value 2';
$value3 = 'value 3';
$valuesArray = array(&$value1, &$value2, &$value3);

call_user_func_array("printVars", $valuesArray);

var_dump($valuesArray);

Вот теперь мы добились нужного результата.

array(3) {
[0]=>
&string(8) "value 1!"
[1]=>
&string(8) "value 2!"
[2]=>
&string(8) "value 3!"
}

Но и это ещё не все проблемы. call_user_func_array возвращает "the function result, or false on error". А если наша функция возвращает false в качестве результата своей работы? Как нам определить, возникала ошибка или нет?

Однозначно определить не получится, но можно хотя бы чуть-чуть улучшить ситуацию при помощи предварительного вызова is_callable.

$handler = "printVars";
if (is_callable($handler)) { 
    $res = call_user_func_array( $handler , $valuesArray );
} else {
    throw new Exception();
}

Теперь мы хотя бы знаем, доступна ли нам вызываемая функция. Может быть, не стоит и пытаться её вызывать.

Третьим вариантом вызова функции с переменным числом параметров является механизм reflection. (Ума не приложу, как корректно перевести термин reflection на русский язык.)
function printVars() {
    $argsCount = func_num_args();
    $argsArray = func_get_args();
    for ($i = 0; $i < $argsCount; $i++) {
        echo $argsArray[$i] . "\n";
    }
}

$valuesArray = array('value 1', 'value 2', 'value 3');

$function_ref = new ReflectionFunction('printVars');
$res = $function_ref->invokeArgs($valuesArray);

Этот код имеет ту же проблему с передачей параметров по ссылке, что и вариант с call_user_func_array, но с возвращением "false" в качестве результата выполнения функции всё хорошо. invokeArgs всегда возвращает результат вызываемой функции, в случае же возникновения ошибки бросается исключение ReflectionException.

А что со скоростью выполнения? Усреднённые результаты двадцати прогонов по 10 000 вызовов одной и той же функции с переменным числом параметров:

  • eval - 0.113432610035 мкс
  • call_user_func_array - 0.0677324056625 мкс
  • reflection - 0.063702750206 мкс

Тесты проводились на PHP 5.3.10. Учитывая сомнительное качество тестового стенда, разностью в скорости работы call_user_func_array и reflection можно смело пренебречь, а вот eval и тут проявил себя хуже всех.

В качестве резюме. Не нужно писать функции с переменным числом параметров. Проблем с ними больше, чем плюсов от их использования, но, если уж пришлось работать с такой функцией, то пробуем использовать в первую очередь механизм reflection. Из имеющихся альтернатив он выглядит наиболее привлекательно.