Есть в 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. Из имеющихся альтернатив он выглядит наиболее привлекательно.
Комментариев нет:
Отправить комментарий