Мой массив ссылок в 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 для тестирования.


Редактировать:

Как указал не один человек, исправление заключается в изменении 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 для равнозначных скаляров)


Примечание администратора: Я бы с удовольствием отдал должное mario the site за этот ответ, поскольку он первым предложил правильный ответ, но поскольку он написал его в комментарии, а не в самом ответе, я не могу этого сделать :-(

11
задан Rick Koshi 1 December 2011 в 05:06
поделиться