Xpoint
   [напомнить пароль]

Элементы массива по ссылке - баг или фича

Метки: [без меток]
2008-11-07 20:54:26 [обр] Александр Носов(6/9)[досье]

Вот пример тестового php-кода:

<?php
class w
{
    private $a = array();
    private $b = array();

    public function init() {
        $this->a = array("x" => 1, "y" => 2, "z" => 3);

        foreach ($this->a as $k => $v) {
           $this->b[$k] =& $this->a[$k];
        }
    }

    public function get1() {
        return $this->a;
    }

    public function get2() {
        return $this->b;
    }

    public function view() {
        echo "<pre>";
        print_r($this->a);
        print_r($this->b);
        echo "</pre>";
    }
}

$o = new w();
$o->init();

$c1 = $o->get1();
$c1["x"] = 5;
$c2 = $o->get2();
$c2["y"] = 6;

$o->view();

?>

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

Вот результат который я вижу на экране:

Array
(
    [x] => 5
    [y] => 6
    [z] => 3
)
Array
(
    [x] => 5
    [y] => 6
    [z] => 3
)

Хотелось-бы понять это баг или фича?

спустя 19 минут [обр] Александр Носов(6/9)[досье]

Создавал тему вроде-бы в разделе: Программирование » PHP
Не пойму каким образом она попала в "Прочее".

Просьба к модератору: перебросить этот вопрос в "Программирование » PHP" и удалить из него эту просьбу.

спустя 1 день 2 часа [обр] Роман(3/3)[досье]
На самом деле вы организовали с помощью знака "=&", так называемую, "жесткую ссылку на объект", то есть "$this->b" является просто синонимом "$this-a". Эти две переменные указывают на одно и тоже место в памяти. Так что, удивляться тут нечему...
спустя 15 минут [обр] Роман(3/3)[досье]
...кстати, убедиться в этом просто. Попробуйте в строке "$c2["y"] = 6;" заменить "у" на "х", в результате вы получите оба значения "х" равным 6ти, то есть было применено последнее изменение из двух, что и требовалось доказать....:0)
спустя 14 часов [обр] Алексей В. Иванов(509/2861)[досье]
М Перенесено из форума "Прочее"
спустя 1 час 9 минут [обр] Александр Носов(6/9)[досье]

Роман[досье]
Именно эти действия я и выполнял для проверки. ;)

Но возвращаю то я массив не по ссылке, а копию. Если бы я вернул этот массив по ссылке, тогда да. А так, IMHO более правильным, было бы, когда я делаю копию массива - все элементы, рекурсивно, становятся копиями.
Ещё более странным для меня, было то, что свойство $this->a тоже превратилось в точно такой-же массив ссылок. Я всегда считал, что при выше приведённой процедуре исходный элемент не меняется, просто "клон"становится ссылкой на него.

Рассмотрим ситуацию:
- я в базовом классе делаю свойством массив (и рассчитываю, что этот массив никак нельзя видоизменить извне);
- делаю метод, который может возвращать данный массив;
- кто-то делает наследника от исходного класса и у себя создает ссылки на элементы массива;
- в итоге, мой метод возвращает совсем не то, что я рассчитывал изначально;
- если теперь в возвращённом массиве изменить какие-то значения, то они поменяются и в свойствах объекта;
- в результате повторного обращения к объекту мы получим уже совсем не те значения.

Собственно, примерно такая ситуация у меня и произошла...

Вероятность, что человек который будет делать "наследника" выполнит подобные действия очень низкая, но она не исключена! Одно дело, когда он будет это делать как "хак" и понимать, что он делает. И совсем другое, когда он сделает это не осознанно. В результате такой баг будет очень сложно отловить.
Сложится впечатление, что не правильно работает базовый класс, хотя проблема будет в наследнике!

Мне теперь что - перед тем как вернуть какой-то массив обязательно рекурсивно обходить все его элементы и делать из них копии? Бред!...
Я все больше склоняюсь к мысли, что это баг в PHP.

спустя 5 часов [обр] Lynn «Кофеман»(98/571)[досье]

$this->a, $this->b, $c1 и $c2 это четыре разных массива, но в каждом из них элементы x, y и z ссылаются на одну и ту же область памяти. Когда делается присваивание $c1 = $o->get1() — создаётся новый массив и в него копируются элементы исходного массива. Проблема в том, что в исходном массиве уже не числа, а ссылки и именно они и копируются.

Это не баг, это задокументированное поведение.

спустя 37 минут [обр] Александр Носов(6/9)[досье]

Lynn «Кофеман»[досье]
Я понимаю, что в $this->b ссылки на ту-же самую область памяти в которой лежат элементы $this->a. Я не понимаю почему нельзя было $this->a оставить в том виде в котором он создавался изначально? Зачем менять исходные данные?

С одной стороны, я конечно вижу определённое удобство в том, что $this->a и $this->b стали абсолютно идентичными, но с другой - это приводит к вышеописанным проблемам.

спустя 4 часа 6 минут [обр] MiRacLe(47/77)[досье]
Вам проще выдумать баг, чем пойти и прочитать что же такое ссылки в PHP ?
спустя 11 часов [обр] Александр Носов(6/9)[досье]
MiRacLe[досье]
Спасибо. Сразу не нашёл этой информации.
Но все равно такое поведение IMHO не всегда можно назвать правильным.
спустя 27 минут [обр] MiRacLe(47/77)[досье]
Вот есть у нас список неких знакомых товарищей: Александр Носов, Носов Александр и xpointUser230.
Если одному из них сломать руку, то болеть будет у всех. Нет никаких противоречий?
Что именно вам не кажется правильным?
спустя 1 час 26 минут [обр] Александр Носов(6/9)[досье]
Что именно вам не кажется правильным?

То что меняются свойства исходного массива.

Я когда создавал его в базовом классе (согласно моему примеру) - я рассчитываю, что метод возвращает обычную копию массива, а он вместо этого возвращает мне массив ссылок.

В итоге полностью ломается логика работы класса.

Может такой подход был и правильным для процедурного программирования, но для OOP он не подходит!

спустя 21 минуту [обр] MiRacLe(47/77)[досье]

Вы так и не поняли.

Метод вернул вам копию СПИСКА, а в списке перечислены некие знакомые товарищи... если одному из них... дальше будет сказочка про белого бычка.

спустя 17 минут [обр] Александр Носов(6/9)[досье]

MiRacLe[досье]
Возможно я Вас не понимаю, а возможно Вы меня.

Моё мнение таково, что массив a не должен видоизменятся, когда в массиве b создаются ссылки на элементы массива a. Но если кому-то "ломают руку" в массиве a, соответственно "ломается рука" у соответствующего элемента в массиве b. И наоборот!

Т.е. логика работы ссылок никак не меняется, кроме одного: массив a как был изначально создан в определённом виде, так в этом виде и остаётся, сколько бы ссылок на его элементы не было создано. А вот массивы b, c и т.д. уже должны содержать ссылки, как это происходит сейчас.

спустя 47 минут [обр] MiRacLe(47/77)[досье]
Вы не понимаете что такое ссылка (вернее понимаете как-то по-своему). Это просто ещё одно "имя" для "товарища", как вы его не зовите, куда не записывайте - это просто имя. При обращении к нему откликается один и тот же "товарищ".
спустя 28 минут [обр] Александр Носов(6/9)[досье]

MiRacLe[досье]
Я все понимаю.
Я пытаюсь объяснить простую вещь:
В массиве a до того как я сделал ссылки из массива b лежали сами элементы и когда я возвращал этот массив, то возвращал копии этих элементов.
Как только я сделал ссылки - массив a уже стал содержать не сами элементы, а ссылки на них. Т.е. по структуре он стал выглядеть точно также как и массив b.

IMHO, более правильным было-бы, чтобы a никак не изменялся, а b содержал ссылки, как и положено. Тогда логика работы программы никак не нарушалась бы.

спустя 3 часа 36 минут [обр] MiRacLe(47/77)[досье]
сообщение промодерировано

Наконец дошло :)

Да, действительно не совсем очевидно почему $c = & $a['b'] меняет сам $a

спустя 3 часа 23 минуты [обр] Роман(3/3)[досье]
если эта ситуация вас так уж "коробит", вместо знака "=&" поставте знак "=", тогда в результате вы действительно получите четыре массива: $this->a, $this->b, $c1,$c2. И все они будут действительно независимы друг от друга.
спустя 19 минут [обр] Александр Носов(6/9)[досье]
Роман[досье] Отличный совет! :))))
спустя 6 часов [обр] Алексей Полушин(62/231)[досье]
Я так понимаю, вопрос в том, почему после получения ссылки на элемент массива, сам этот элемент массива меняется на ссылку ?
$a=array(1,2);
var_dump($a);
$b=&$a[0];
var_dump($a);
спустя 20 минут [обр] Алексей Полушин(62/231)[досье]
http://bugs.php.net/bug.php?id=20993
Баг решено не исправлять, а задокументировать. Так что это не баг, а фича :)
спустя 50 минут [обр] MiRacLe(47/77)[досье]
Решено, в 2005-ом году, но что-то в документации это так и не появилось (есть ссылка на описание бага в комментариях) ...
спустя 1 час 56 минут [обр] Александр Носов(6/9)[досье]
Баг решено не исправлять, а задокументировать. Так что это не баг, а фича
Задокументировать конечно проще, чем исправить.
Интересная, конечно, история у этого бага.
Только, повторюсь, такой подход в эпоху процедурного программирования - ещё мог как-то прокатить, но для OOP это может быть катастрофой! Когда из-за такого вот бага десятки часов могут быть убиты на поиск проблемы. И хуже всего если это будет обнаружено в уже сданно проекте.
спустя 3 минуты [обр] Lynn «Кофеман»(98/571)[досье]

Задокументировано.

http://www.php.net/manual/en/language.references.whatdo.php

Note: If array with references is copied, its values are not dereferenced. This is valid also for arrays passed by value to functions.
спустя 4 минуты [обр] Александр Носов(6/9)[досье]

Lynn «Кофеман»[досье] Согласен. Я видел этот комментарий и русской версии.
Но от этого не легче...

Думаю, что правильнее было-бы из "шестерки" убрать этот баг. Тем более, что они в 6-й версии собираются существенно доработать работу со ссылками.

P.S. Я проверил но той "шестерке", что они сейчас предлагают - этот баг существует.

спустя 4 минуты [обр] Александр Носов(6/9)[досье]
Точнее не так!
Я не против того чтобы при копировании ссылки не разыменовывались.
Я против того, что исходный массив видоизменяется!
спустя 7 минут [обр] MiRacLe(47/77)[досье]
сообщение промодерировано
Lynn «Кофеман»[досье] речь не о том, что array with refrences, а о том, что if a reference is made to a single element of an array and then the array is copied, whether by assignment or when passed by value in a function call, *the reference is copied as part of the array*
спустя 1 час 2 минуты [обр] Михаил Кюршин aka ya-ya(69/414)[досье]
Не вижу ничего странного. В перле, например, то же самое. Если вы копируете массив, который содержит ссылки, совершенно логично ожидать, что в новом массиве тоже будут содержаться ссылки (что копировали то и получили), и что изменение элементов в новом, скопированном массиве — это изменение значений, на которые ссылаются элементы этого массива, т.е. тех же значений, на которые ссылаются элементы исходного массива. Рассматривайте ссылку не как ссылку на переменную, а как ссылку на область памяти, где лежит значение, на которое также ссылается (на абсолютно тех же правах) и исходная переменная.
Было бы как раз странно, если при заполнении массива ссылками при копирвании этого массива мы бы получали в массиве скопированные значения этих ссылок.
спустя 35 минут [обр] Алексей Полушин(62/231)[досье]
Михаил Кюршин aka ya-ya[досье]
Не о том речь. Понятно, что если копировать массив, который содержит ссылки, в новом массиве тоже будут ссылки.
Был массив, не содержащий ссылок. При его копировании копировались значения.
Потом мы получили ссылку на один из элементов массива. Что теперь должно происходить при копировании этого массива ?
спустя 23 минуты [обр] Михаил Кюршин aka ya-ya(69/414)[досье]

Алексей Полушин[досье]
Я понял, о чём речь. Просто вы рассматриваете изначальный массив, как массив, содержащий значения. Но это не совсем так.
Давайте от простого к сложному. Я буду упрощать в техническом плане, но постараюсь не ошибаться в идее.

Когда вы создаёте перемененную
$var = "1";
в некую область памяти попадает значение "1". У этой области памяти есть адрес, допустим X.

Далее, когда вы создаёте перемененную
$var2 =& $var;

создаётся ссылка. НО! не ссылка на первую переменную, а ссылка на эту область памяти. Поэтому, технически, после создания ссылки $var и $var1 получают одинаковые права на область памяти X, т.к. они обе ссылаются на эту область. Именно поэтому, меняя значение $var мы добьёмся изменения значения $var1 и наоборот.

Теперь к вашему примеру из первого сообщения.

Сначала вы создаёте массив $a, заполняя его значениями.

Затем вы в цикле создаёте в массиве $b элементы-ссылки на элементы второго массива. Если воспользоваться подходом, описанным выше, что происходит? а происходит создание второй переменной, указывающей на определённую область памяти. И поэтому первый массив теперь также содержит ссылки на области памяти — такие же по "правам" — как и второй массив.

Моя мысль ясна?

спустя 9 минут [обр] Александр Носов(6/9)[досье]

Михаил Кюршин aka ya-ya[досье]
Мысль ясна с технической точки зрения. Но с прикладной - свойства массива в корне изменились, после того как кто-то сделал на него ссылку.

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

Возникает небольшое неудобство. Я должен все время помнить про вышеописанную особенность и каждый раз, когда возвращаю какой-то массив, который является свойством данного объекта - возврат делать через эту функцию.

Но при этом у меня появятся различные возможности по работе с такими массивами.

Ваше мнение?

спустя 1 минуту [обр] MiRacLe(47/77)[досье]
Михаил Кюршин aka ya-ya[досье]
$a=array(1,2);
var_dump($a);
$b=&$a[0];
var_dump($a);
//unset($b);
$c = $a;
$c[0] = 3;
var_dump($a);
спустя 11 минут [обр] Михаил Кюршин aka ya-ya(69/414)[досье]
MiRacLe[досье] вы по ссылке копируете массив, а сам массив содержит значения, но не ссылки, и ссылки на элементы массива не создаются. Ваш пример полностью противоположен исходному: там массив копируется по значению, а все его элементы — ссылки. Попробуйте вот это, это иллюстрирует моё предыдущее сообщение:
<pre>
<?php
$a=array(1,2,3);
$b=array();
$c=array();
foreach ($a as $k=>$v) {
    $b[$k] =& $a[$k];
    $c[$k] = $a[$k];
}

$b[0] = '4';
$b[1] = '5';
$b[2] = '6';

print_r($a);
print_r($b);
print_r($c);
?>
</pre>
спустя 3 минуты [обр] Михаил Кюршин aka ya-ya(69/414)[досье]
Александр Носов[досье] сложно сказать, в принципе, тот факт, что реализация вашего проекта упирается в какие-то особенности языка говорит не о самой оптимальной проработке архитектуры. В большом проекте всё должно быть чётко, понятно и просто как гвозди и молоток, а такие кульбиты слишком неочевидны и могут привести к непредсказуемым последствиям. Попробуйте упростить задачи. Не знаю.
спустя 32 минуты [обр] MiRacLe(47/77)[досье]
Михаил Кюршин aka ya-ya[досье]
Мой пример показывает проблему, которую нам пытается донести Александр Носов[досье] - при создании ссылки на элемент массива, сам элемент заменяется ссылкой. Она-то и создаёт описанные side-effects.
спустя 4 минуты [обр] Михаил Кюршин aka ya-ya(69/414)[досье]
MiRacLe[досье] я проблему Александра понял, и написал, почему на мой взгляд так происходит. И что это проблемой нельзя считать, и что так и должно быть, и что так во всех языках. При создании ссылки (неважно на что, на массив, на элемент массива и т.п.) после этого вы не сможете определить, что первично — созданая изначально переменная (или элемент массива) или созданная потом ссылка. Потому что технически — это уже не переменная и ссылка на неё, это область памяти и две ссылки на неё.
спустя 1 час 4 минуты [обр] Александр Носов(6/9)[досье]

Михаил Кюршин aka ya-ya[досье] Я бы не сказал что мы упёрлись в проблему...
Хотя все началось с того, что мы наступили на "подобные грабли". Но причину мы нашли и пофиксили довольно быстро.

Сейчас меня эта проблема интересует скорее теоретически - как защититься от повторных "наступаний на такие-же грабли".

спустя 48 минут [обр] Алексей Полушин(62/231)[досье]

Михаил Кюршин aka ya-ya[досье]
Ну почему во всех языках ?
php:

$a=array(1,2);
$s=&$a[0];
$b = $a;
$b[0] = 3;
echo $a[0];

получим 3
perl:

@a = (1,2);
$s = \$a[0];
@b = @a;
$b[0]=3;
print $a[0];

получим 1
В С++ такого эффекта тоже нет

спустя 23 минуты [обр] Михаил Кюршин aka ya-ya(69/414)[досье]
Алексей Полушин[досье] согласен, с перлом я ошибся, проверял по-другому и поторопился с выводами
спустя 18 часов [обр] Александр Носов(6/9)[досье]

Народ, правильным ли будет такой вывод из всего нашего обсуждения?

Учитывая следующие факты:

  • такое поведение при работе со ссылками НЕ является характерным для других языков;
  • изначально это собирались фиксить как баг, и только потом решили "узаконить" как фичу;
  • в PHP6 собирались существенно доработать работу со ссылками;
  • такое поведение может нарушить логику работы класса, при создании ссылок в наследнике;

предложить сделать доработку в PHP одним из следующих способов:

  1. При помощи конфигурационных параметров (в php.ini или через ini_set) изменять режим работы php таким образом, чтобы свойства исходных элементов массива не менялись, при создании ссылок на них.
  2. Создать дополнительную функцию, при помощи которой можно быстро, рекурсивно, выполнить разыменование ссылок.
  3. Свой вариант.

1-й вариант мне не нравится тем, что будет не совместимость с предыдущими версиями php-программ. Но учитывая тот факт, что при переходе на новую версию, у нас практически всегда так происходит - тут большой беды не будет.
2-й вариант плох своей не надёжностью и не глобальностью решения проблемы.

Или ваш вариант? ;)

Powered by POEM™ Engine Copyright © 2002-2005