Форум программистов «Весельчак У»
  *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

  • Рекомендуем проверить настройки временной зоны в вашем профиле (страница "Внешний вид форума", пункт "Часовой пояс:").
  • У нас больше нет рассылок. Если вам приходят письма от наших бывших рассылок mail.ru и subscribe.ru, то знайте, что это не мы рассылаем.
   Начало  
Наши сайты
Помощь Поиск Календарь Почта Войти Регистрация  
 
Страниц: [1]   Вниз
  Печать  
Автор Тема: Странное поведение PHP. Ссылки и массивы.  (Прочитано 12576 раз)
0 Пользователей и 1 Гость смотрят эту тему.
RXL
Технический
Администратор

ru
Offline Offline
Пол: Мужской

WWW
« : 27-04-2012 12:57 » 

Разработчики PHP многое плохо продумали, слизывая с других языков и упрощая в угоду синтаксису. Массивы и ссылки - оно из таких заимствований. Все нижеописанное не новое, но лично я с этими особенностями сталкиваюсь так редко, что успеваю забыть и каждый раз получается как заново. Все ниже перечисленные вещи описаны в мануале, комментариях к мануалу или просто их уже кто-то разбирал и описал это в инете.


1. Ссылки в foreach.

Код: (PHP)
foreach ($array as $value) // $value - копия элемента массива

foreach ($array as &$value) // $value - ссылка на элемент массива

Фишка: специальный синтаксис для перебора значений по ссылке. Например, присваивание нового значения можно сделать только по ссылке.


2. Ссылки: присвоение ссылки и присвоение по ссылке.

Код: (PHP)
foreach ($list1 as &$val)
 /* ... */ ;

foreach ($list2 as $val)
  $val *= 10;

// Последний элемент $list1 равен (10 * последний элемент $list2).

Фишка: по ошибке, во втором foreach, вместо ссылки выборка идет по значению и $val по прежнему ссылается на последний элемент массива $list1, который претерпевает метаморфозы. Более того, во втором цикле может и не ошибка была:

Код: (PHP)
foreach ($list1 as &$val)
 /* ... */ ;

foreach ($list2 as $val)
 /* ... */ ;

// Последний элемент $list1 равен последнему элементу $list2.


3. Встроенные функции работы с массивом.

Не поручусь за все, но push/pop/shift/unshift - точно. Они пересоздают архив по значению.

Код: (PHP)
$a = 1;
$b = 2;
$list = array(&$a);
array_push($list, $b);

После array_push ссылка на $a заменяется значением.

Код: (PHP)
$a = 1;
$b = 2;
$list = array(&$a, &$b);
$c = array_shift($list);

В $c будет значение $a, а в массиве ссылка на $b заменится значением.

По этому данные функции лучше не использовать. Они могут испортить структуру и снижают производительность.
Замена довольно громоздкая:

Код: (PHP)
$a = 1;
$b = 2;
$list = array(&$a);

// push для ссылки
$list[] = &$b;

// shift для ссылки
$key = key($list);
$c = &$list[$key];
unset($list[$key]);

Замечу, что key() сообщает ключ не на первого, а текущего элемента.
UPD: вместо key() лучше использовать reset() - он точно вернет первый элемент.


И после всего этого говорят, что PHP прост, а Perl сложен! Да в Перле push(@array, $ref) никогда не поместит в массив значение, а shift(@array) не будет колбасить весь массив.




Ссылки - вещь нужная. Они упрощают алгоритмы, уменьшают объем данных. Надо только вовремя вспомнить о подводных камнях.

Например, на входе массив с ответами JSON RPC 2.0. Нужно перекодировать строки. Я выбрал итеративный метод.

Код: (PHP)
        // Перекодировка строк в местную кодировку.
        if (strcasecmp($this->charset, 'utf-8'))
        {
            $queue = array(&$responses);

            while (count($queue))
            {
                $key = key($queue);
                $obj = &$queue[$key];
                unset($queue[$key]);

                foreach ($obj as &$val)
                    switch (gettype($val))
                    {
                        case 'string':
                            $val = iconv('UTF-8', $this->charset, $val);
                            break;
                        case 'array':
                            $queue[] = &$val;
                            break;
                    }
            }
        }
« Последнее редактирование: 29-04-2012 18:12 от RXL » Записан

... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С.
RXL
Технический
Администратор

ru
Offline Offline
Пол: Мужской

WWW
« Ответ #1 : 30-10-2012 16:50 » 

Вот снова столкнулся со странностями работы с массивами.

Немного в сторону: методы типа array_push, array_shift и т.п. полностью пересоздают массив, когда как "$array[] = ..." и "unset($array['key'])" изменяют существующий.

И так, о странностях. Цикл foreach создает копию массива. Можно находу в цикле удалять элементы исходного массива, изменять их значения, да и сам массив можно удалить, но на работу цикла это не повлияет. Что интересно, объекта класса с интерфейсом Iterator это не касается.
Примеры:

Код: (PHP)
$list = array(1, 2, 3);

foreach ($list as $k => $v) {
    $list = 'abc';
    echo "$k: $v\n";
}

/*
Результат:
0: 1
1: 2
2: 3
*/

Код: (PHP)
$list = array(1, 2, 3);

foreach ($list as $k => $v) {
    $list[count($list) - 1 - $k]++;
    echo "$k: $v\n";
}

/*
Результат:
0: 1
1: 2
2: 3
*/

А вот если использовать в цикле ссылку на значение, то эффект частично пропадает.
Пример:

Код: (PHP)
$list = array(1, 2, 3);

foreach ($list as $k => &$v) {
    $list[count($list) - 1 - $k]++;
    echo "$k: $v\n";
}

/*
Результат:
0: 1
1: 3
2: 4
*/
« Последнее редактирование: 30-10-2012 17:14 от RXL » Записан

... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С.
Sla
Команда клуба

ua
Offline Offline
Пол: Мужской

WWW
« Ответ #2 : 30-10-2012 16:55 » 

т.е. ты хочешь сказать что
foreach $array as $key=>$value {
unset($array[$key])
}
не "удалит" элемент?
Записан

Мы все учились понемногу... Чему-нибудь и как-нибудь.
RXL
Технический
Администратор

ru
Offline Offline
Пол: Мужской

WWW
« Ответ #3 : 30-10-2012 16:58 » new

Удалит. Но в цикле ты его все равно обработаешь.

У меня была задача такого плана: список структур, которые надо было сгруппировать по некому признаку. Получился вложенный цикл. Во внутреннем цикле я помечал элементы удаленными (сперва я тоже пытался делать unset), чтобы не обработать их повторно. Оказалось, что $list[$k] видит изменение значений, а foreach, в котором сейчас находишься, не видит.

Проблема решается как и в первом посте: использованием ссылок в цикле. И не забываем удалить ссылку после цикла.
Вариант без ссылок:

Код: (PHP)
$list = array(
    array('type' => 1, 'val' => 5),
    array('type' => 2, 'val' => 10),
    array('type' => 1, 'val' => -2),
    array('type' => 3, 'val' => 20),
);

foreach ($list as $key => $item) {
    echo "Fetch: {$item['type']}, {$item['val']}: ";

    if (!empty($list[$key]['deleted'])) {
        echo "skip\n";
        continue;
    }

    echo "continue\n";
    $list[$key]['deleted'] = 1;

    foreach ($list as $key2 => $item2) {
        if (!empty($list[$key2]['deleted']))
            continue;

        if ($item2['type'] != $item['type'])
            continue;

        $list[$key]['val'] += $item2['val'];
        $list[$key2]['deleted'] = 1;
    }
}

foreach ($list as $item)
    printf("%1u %3d %1u\n", $item['type'], $item['val'], !empty($item['deleted']));

/*
Fetch: 1, 5: continue
Fetch: 2, 10: continue
Fetch: 1, -2: skip
Fetch: 3, 20: continue
1   3 1
2  10 1
1  -2 1
3  20 1
*/

Вариант с ссылками:

Код: (PHP)
$list = array(
    array('type' => 1, 'val' => 5),
    array('type' => 2, 'val' => 10),
    array('type' => 1, 'val' => -2),
    array('type' => 3, 'val' => 20),
);

foreach ($list as &$item) {
    echo "Fetch: {$item['type']}, {$item['val']}: ";

    if (!empty($item['deleted'])) {
        echo "skip\n";
        continue;
    }

    echo "continue\n";
    $item['deleted'] = 1;

    foreach ($list as &$item2) {
        if (!empty($item2['deleted']))
            continue;

        if ($item2['type'] != $item['type'])
            continue;

        $item['val'] += $item2['val'];
        $item2['deleted'] = 1;
    }
}

unset($item);

foreach ($list as $item)
    printf("%1u %3d %1u\n", $item['type'], $item['val'], !empty($item['deleted']));

/*
Fetch: 1, 5: continue
Fetch: 2, 10: continue
Fetch: 1, -2: skip
Fetch: 3, 20: continue
1   3 1
2  10 1
1  -2 1
3  20 1
*/


Добавлено через 10 часов, 36 минут и 50 секунд:
У меня получилось такое заключение:
Форма "foreach (list as val)" создает временную копию массива с копиями значений.
Форма "foreach (list as &val)" не создает копию массива со ссылками на исходные значения.


Но проверка работы лимитов памяти не подтверждает, а еще больше запутывает.
Пример. В настройках memory_limit = 128M. Смысл теста в том, чтобы создать условия, когда на копию объекта не хватит памяти.

Код: (PHP)
$memsize = 128 * 1024 * 1024;
$list = array(
    str_repeat('1', $memsize * 0.4),
    str_repeat('1', $memsize * 0.4)
);

foreach ($list as $v) {
    echo strlen($v), "\n";
}

Как ни странно, тест проходит. А вот этот падает на строке 8.

Код: (PHP)
  1. $memsize = 128 * 1024 * 1024;
  2. $list = array(
  3.     str_repeat('1', $memsize * 0.35),
  4.     str_repeat('1', $memsize * 0.35)
  5. );
  6.  
  7. foreach ($list as &$v) {
  8.     echo strlen($v), "\n";
  9. }


Добавлено через 3 часа, 24 минуты и 35 секунд:
И вот еще интересный аспект влияния цикла на время жизни объекта.

Код: (PHP)
  1. class X
  2. {
  3.     private $name;
  4.  
  5.     public function __construct($name) {
  6.         $this->name = $name;
  7.         echo "++ $this->name\n";
  8.     }
  9.  
  10.     public function __destruct() {
  11.         echo "-- $this->name\n";
  12.     }
  13. }
  14.  
  15. $list = array(new X('old_1'), new X('old_2'));
  16.  
  17. foreach ($list as $k => $v) {
  18. //    $list[$k] = null;
  19.     $list[$k] = new X('new_' . ($k + 1), 0);
  20. }

Строка 18 не влияет на результат: объекты будут уничтожены после цикла, по завершению программы:

++ old_1
++ old_2
++ new_1
++ new_2
-- old_1
-- old_2
-- new_1
-- new_2


Скажу более: если обнуление заменить на unset(), то результат также не изменится.

Код: (PHP)
  1. class X
  2. {
  3.     private $name;
  4.  
  5.     public function __construct($name) {
  6.         $this->name = $name;
  7.         echo "++ $this->name\n";
  8.     }
  9.  
  10.     public function __destruct() {
  11.         echo "-- $this->name\n";
  12.     }
  13. }
  14.  
  15. $list = array(new X('old_1'), new X('old_2'));
  16.  
  17. foreach ($list as $k => &$v) {
  18. //    $list[$k] = null;
  19.     $list[$k] = new X('new_' . ($k + 1), 0);
  20. }

А здесь влияет. Без "обнуления": создается новый объект, после чего уничтожается старый.

++ old_1
++ old_2
++ new_1
-- old_1
++ new_2
-- old_2
-- new_2
-- new_1


С "обнулением": сперва уничтожается старый, потом создается новый.

++ old_1
++ old_2
-- old_1
++ new_1
-- old_2
++ new_2
-- new_1
-- new_2


А если тут применить unset(), то присвоение в строке 19 добавляет элемент в массив в конец списка, что приводит к дополнительной итерации цикла, в которой добавляется еще один элемент и т.д. Т.е. с такой формой цикла массив можно использовать как самопополняемую очередь заданий.
« Последнее редактирование: 30-10-2012 20:23 от RXL » Записан

... мы преодолеваем эту трудность без синтеза распределенных прототипов. (с) Жуков М.С.
Страниц: [1]   Вверх
  Печать  
 

Powered by SMF 1.1.21 | SMF © 2015, Simple Machines