Мой массив ссылок PHP "магически" становится массивом ценностей... почему?

Я создаю функцию-оболочку вокруг mysqli, чтобы мое приложение не было чрезмерно сложным с кодом обработки базы данных. Часть этого кода - это немного кода для параметризации вызовов SQL с помощью mysqli:: bind_param(). bind_param(), как вы знаете, требует ссылок. Поскольку это полуобщественная оболочка, я в конечном итоге делаю этот вызов:

call_user_func_array(array($stmt, 'bind_param'), $this->bindArgs);

и я получаю сообщение об ошибке:

Parameter 2 to mysqli_stmt::bind_param() expected to be a reference, value given

Вышеупомянутое обсуждение заключается в том, чтобы установить тех, кто сказал бы: "Вам не нужны ссылки вообще в вашем примере".

Мой "настоящий" код немного сложнее, чем кто-либо хочет читать, поэтому я закипил код, приводящий к этой ошибке, на следующий (надеюсь) иллюстративный пример:

class myclass {
  private $myarray = array();

  function setArray($vals) {
    foreach ($vals as $key => &$value) {
      $this->myarray[] =& $value;
    }
    $this->dumpArray();
  }
  function dumpArray() {
    var_dump($this->myarray);
  }
}

function myfunc($vals) {
  $obj = new myclass;
  $obj->setArray($vals);
  $obj->dumpArray();
}

myfunc(array('key1' => 'val1',
             'key2' => 'val2'));

Проблема заключается в том, что в myfunc() между вызовом setArray() и вызовом dumpArray() все элементы в $obj- > myarray перестают быть ссылками и становятся просто значениями. Это можно легко увидеть, посмотрев на результат:

array(2) {
  [0]=>
  &string(4) "val1"
  [1]=>
  &string(4) "val2"
}
array(2) {
  [0]=>
  string(4) "val1"
  [1]=>
  string(4) "val2"
}

Обратите внимание, что массив находится в "правильном" состоянии в первой половине вывода здесь. Если это имеет смысл сделать это, я мог бы сделать свой вызов bind_param() в этот момент, и это сработает. К сожалению, во второй половине выпуска что-то ломается. Обратите внимание на отсутствие "&" по типам значений массива.

Что случилось с моими рекомендациями? Как я могу предотвратить это? Мне не нравится называть "PHP-ошибку", когда я действительно не специалист по языку, но может ли это быть одним? Мне это кажется очень странным. Я использую PHP 5.3.8 для моего тестирования на данный момент.


Edit:

Как указывалось более чем одним человеком, исправление заключается в том, чтобы изменить setArray(), чтобы принять его аргумент по ссылке:

function setArray(&$vals) {

Я добавляю эту заметку в документ, ПОЧЕМУ это кажется сработавшим.

В общем, PHP и mysqli, в частности, имеют немного странную концепцию того, что такое "ссылка". Обратите внимание на этот пример:

$a = "foo";
$b = array(&$a);
$c = array(&$a);
var_dump($b);
var_dump($c);

Прежде всего, я уверен, что вам интересно, почему я использую массивы вместо скалярных переменных - это потому, что var_dump() не показывает никаких признаков того, является ли скаляр ссылкой, но он делает для элементов массива.

В любом случае, в этот момент $b [0] и $c [0] являются ссылками на $a. Все идет нормально. Теперь мы бросаем наш первый ключ в работу:

unset($a);
var_dump($b);
var_dump($c);

$b [0] и $c [0] оба являются ссылками на одно и то же. Если мы изменим один, оба будут по-прежнему меняться. Но на что они ссылаются? Некоторое неназванное местоположение в памяти. Конечно, сбор мусора гарантирует, что наши данные будут безопасными и останутся такими, пока мы не перестанем ссылаться на него.

Для нашего следующего трюка мы делаем следующее:

unset($b);
var_dump($c);

Теперь $c [0] является единственной ссылкой на наши данные. И, эй! Магически это уже не "ссылка". Не с помощью меры var_dump(), а не с помощью mysqli:: bind_param().

руководство по PHP говорит, что на каждом фрагменте данных имеется отдельный флаг, 'is_ref'. Однако этот тест показывает, что 'is_ref' на самом деле эквивалентен '(refcount > 1)'

Для удовольствия вы можете изменить этот пример игрушки следующим образом:

$a = array("foo");
$b = array(&$a[0]);
$c = array(&$a[0]);

var_dump($a);
var_dump($b);
var_dump($c);

Обратите внимание, что все три массива имеют ссылочную метку на своих членах, что подтверждает идею о том, что 'is_ref' функционально эквивалентен '(refcount > 1)'.

Это вне меня, почему mysqli:: bind_param() будет заботиться об этом различии в первую очередь (или, возможно, это call_user_func_array()... в любом случае), но похоже, что нам "действительно" нужно обеспечить что счетчик ссылок составляет не менее 2 для каждого члена $this- > bindArgs в нашем вызове call_user_func_array() (см. самое начало сообщения/вопроса). И самый простой способ сделать это (в данном случае) - сделать setArray() pass-by-reference.


Edit:

Для дополнительной забавы и игр я изменил мою оригинальную программу (не показан здесь), чтобы оставить ее эквивалентом setArray() pass-by-value и создать бесплатный дополнительный массив bindArgsCopy, содержащий точно такую ​​же вещь, как bindArgs, Это означает, что да, оба массива содержат ссылки на "временные" данные, которые были освобождены ко времени второго вызова. Как и было предсказано выше, это сработало. Это демонстрирует, что приведенный выше анализ не является артефактом внутренних действий var_dump() (как бы облегчением для меня), и он также демонстрирует, что это значение ссылки, которое имеет значение, а не "временная" первоначальная память данных.

Итак. Я делаю следующее утверждение: в PHP, с целью call_user_func_array() (и, вероятно, больше), говоря, что элемент данных является "ссылкой", это то же самое, что сказать, что счетчик ссылок элемента больше или равен 2 (игнорируя оптимизацию внутренней памяти PHP для равнозначных скаляров)


Замечание администратора: я хотел бы дать марио оценку сайта для ответа, так как он первым предложил правильный ответ, но так как он написал это в комментарии, а не фактический ответ, я не мог этого сделать это: - (

Ответ 1

Передайте массив как ссылку:

  function setArray(&$vals) {
    foreach ($vals as $key => &$value) {
      $this->myarray[] =& $value;
    }
    $this->dumpArray();
  }

Мое предположение (что может быть неправильным в некоторых деталях, но, по всей видимости, исправлено по большей части), почему это заставляет ваш код работать так, как ожидалось, - это то, что когда вы передаете как значение, все круто для вызова dumpArray() внутри setArray(), потому что ссылка на массив $vals внутри setArray() все еще существует. Но когда управление возвращается к myfunc(), тогда эта временная переменная исчезает, как и все ссылки на нее. Таким образом, PHP добросовестно изменяет массив на строковые значения вместо ссылок, прежде чем освобождать память для него. Но если вы передадите его как ссылку из myfunc(), то setArray() использует ссылки на массив, который живет, когда управление возвращается к myfunc().

Ответ 2

Добавление & в подпись аргумента фиксировало это для меня. Это означает, что функция получит адрес памяти исходного массива.

function setArray(&$vals) {
// ...
}

CodePad.

Ответ 3

Я только столкнулся с этой же проблемой почти в той же ситуации (я пишу обертку PDO для своих целей). Я подозревал, что PHP меняет ссылку на значение, как только никакие другие переменные не ссылаются на одни и те же данные, а Рик, ваши комментарии в редактировании на ваш вопрос подтвердили это подозрение, поэтому спасибо за это.

Теперь, для любого другого, кто сталкивается с чем-то подобным, я считаю, что у меня есть самое простое решение: просто установите каждый соответствующий элемент массива в ссылку на себя, прежде чем передавать массив в call_user_func_array().

Я не уверен, что происходит внутри, потому что кажется, что это не должно работать, но элемент снова становится ссылкой (которую вы можете видеть с помощью var_dump()), а call_user_func_array() затем передает аргумент по ссылке как и ожидалось. Это исправление работает, даже если элемент массива все еще является ссылкой, поэтому вам не нужно сначала проверять.

В коде Rick это будет что-то вроде этого (все после первого аргумента для bind_param по ссылке, поэтому я пропущу первый и исправлю все из них после этого):

for ($i = 1, $count = count($this->bindArgs); $i < $count; $i++) {
    $this->bindArgs[$i] = &$this->bindArgs[$i];
}

call_user_func_array(array($stmt, 'bind_param'), $this->bindArgs);