Мой массив ссылок 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); с unset($a); var_dump($b); var_dump($c); 

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

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

 unset($b); var_dump($c); с 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 () … в любом случае), но, похоже, что мы действительно должны убедиться в том, что ссылка count не менее 2 для каждого члена $ this-> bindArgs в нашем вызове call_user_func_array () (см. самое начало сообщения / вопроса). И самый простой способ сделать это (в данном случае) – сделать setArray () pass-by-reference.


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

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

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


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

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

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

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

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

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

CodePad .

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

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

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

В коде Рика это будет что-то вроде этого (все после первого аргумента для 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);