Как изменить ключи и значения массива при использовании RecursiveArrayIterator?

Я подозреваю, что делаю здесь какую-то глупость, но меня смущает то, что кажется простой проблемой с SPL:

Как изменить содержимое массива (значения в этом примере), используя RecursiveArrayIterator / RecursiveIteratorIterator?

Используя следующий тестовый код, я могу изменить значение в цикле, используя getInnerIterator() и offsetSet() и вывести измененный массив, пока я нахожусь в цикле.

Но когда я выхожу из цикла и выгружаю массив из итератора, он возвращается к исходным значениям. Что творится?

$aNestedArray = array();
$aNestedArray[101] = range(100, 1000, 100);
$aNestedArray[201] = range(300, 25, -25);
$aNestedArray[301] = range(500, 0, -50);

$cArray = new ArrayObject($aNestedArray);
$cRecursiveIter = new RecursiveIteratorIterator(new RecursiveArrayIterator($cArray), RecursiveIteratorIterator::LEAVES_ONLY);

// Zero any array elements under 200  
while ($cRecursiveIter->valid())
{
    if ($cRecursiveIter->current() < 200)
    {
        $cInnerIter = $cRecursiveIter->getInnerIterator();
        // $cInnerIter is a RecursiveArrayIterator
        $cInnerIter->offsetSet($cInnerIter->key(), 0);
    }

    // This returns the modified array as expected, with elements progressively being zeroed
    print_r($cRecursiveIter->getArrayCopy());

    $cRecursiveIter->next();
}

$aNestedArray = $cRecursiveIter->getArrayCopy();

// But this returns the original array.  Eh??
print_r($aNestedArray);

person John Carter    schedule 04.08.2009    source источник
comment
Похоже, это ошибка, которую вы должны сообщить на bugs.php.net.   -  person null    schedule 13.01.2010


Ответы (7)


Кажется, что значения в простых массивах не поддаются изменению, потому что они не могут быть переданы по ссылке конструктору ArrayIterator (RecursiveArrayIterator наследует свои offset*() методы от этого класса, см. Справочник по SPL). Таким образом, все вызовы offsetSet() работают с копией массива.

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

Еще немного кода, чтобы проиллюстрировать это:

$a = array();

// Values inside of ArrayObject instances will be changed correctly, values
// inside of plain arrays won't
$a[] = array(new ArrayObject(range(100, 200, 100)),
             new ArrayObject(range(200, 100, -100)),
             range(100, 200, 100));
$a[] = new ArrayObject(range(225, 75, -75));

// The array has to be
//     - converted to an ArrayObject or
//     - returned via $it->getArrayCopy()
// in order for this field to get handled properly
$a[] = 199;

// These values won't be modified in any case
$a[] = range(100, 200, 50);

// Comment this line for testing
$a = new ArrayObject($a);

$it = new RecursiveIteratorIterator(new RecursiveArrayIterator($a));

foreach ($it as $k => $v) {
    // getDepth() returns the current iterator nesting level
    echo $it->getDepth() . ': ' . $it->current();

    if ($v < 200) {
        echo "\ttrue";

        // This line is equal to:
        //     $it->getSubIterator($it->getDepth())->offsetSet($k, 0);
        $it->getInnerIterator()->offsetSet($k, 0);
    }

    echo ($it->current() == 0) ? "\tchanged" : '';
    echo "\n";
}

// In this context, there's no real point in using getArrayCopy() as it only
// copies the topmost nesting level. It should be more obvious to work with $a
// itself
print_r($a);
//print_r($it->getArrayCopy());
person mermshaus    schedule 11.08.2009

Не использовать классы Iterator (которые, кажется, копируют данные в RecursiveArrayIterator::beginChildren() вместо передачи по ссылке.)

Вы можете использовать следующее, чтобы достичь того, чего вы хотите

function drop_200(&$v) { if($v < 200) { $v = 0; } }

$aNestedArray = array();
$aNestedArray[101] = range(100, 1000, 100);
$aNestedArray[201] = range(300, 25, -25);
$aNestedArray[301] = range(500, 0, -50);

array_walk_recursive ($aNestedArray, 'drop_200');

print_r($aNestedArray);

или используйте create_function() вместо создания функции drop_200, но ваш пробег может варьироваться в зависимости от функции create_function и использования памяти.

person null    schedule 13.01.2010
comment
Просто потратил час на эти чертовы классы SPL Iterator - вероятно, не в первый раз - когда array_walk_recursive отлично справляется с задачей и с меньшим количеством экземпляров/LOC. Я думаю, там есть урок... В любом случае, спасибо! - person Darragh Enright; 20.03.2015
comment
Это не самое идеальное решение, потому что в рекурсии вы не можете проверить тип данных элементов. Во-вторых, лучше использовать итератор, потому что это больше oop. - person schellingerht; 07.06.2017

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

Это действительно полезно для слияния и замены массивов неограниченного уровня в массивах или значениях внутри массивов. К сожалению, array_walk_recursive НЕ будет работать в этом случае, поскольку эта функция посещает только конечные узлы.. поэтому ключ 'replace_this_array' в $array ниже никогда не будет посещен.

Например, чтобы заменить все значения в массиве неизвестных уровней глубины, но только те, которые содержат определенный ключ, вы должны сделать следующее:

$array = [
    'test' => 'value',
    'level_one' => [
        'level_two' => [
            'level_three' => [
                'replace_this_array' => [
                    'special_key' => 'replacement_value',
                    'key_one' => 'testing',
                    'key_two' => 'value',
                    'four' => 'another value'
                ]
            ],
            'ordinary_key' => 'value'
        ]
    ]
];

$arrayIterator = new \RecursiveArrayIterator($array);
$completeIterator = new \RecursiveIteratorIterator($arrayIterator, \RecursiveIteratorIterator::SELF_FIRST);

foreach ($completeIterator as $key => $value) {
    if (is_array($value) && array_key_exists('special_key', $value)) {
        // Here we replace ALL keys with the same value from 'special_key'
        $replaced = array_fill(0, count($value), $value['special_key']);
        $value = array_combine(array_keys($value), $replaced);
        // Add a new key?
        $value['new_key'] = 'new value';

        // Get the current depth and traverse back up the tree, saving the modifications
        $currentDepth = $completeIterator->getDepth();
        for ($subDepth = $currentDepth; $subDepth >= 0; $subDepth--) {
            // Get the current level iterator
            $subIterator = $completeIterator->getSubIterator($subDepth); 
            // If we are on the level we want to change, use the replacements ($value) other wise set the key to the parent iterators value
            $subIterator->offsetSet($subIterator->key(), ($subDepth === $currentDepth ? $value : $completeIterator->getSubIterator(($subDepth+1))->getArrayCopy()));
        }
    }
}
return $completeIterator->getArrayCopy();
// return:
$array = [
    'test' => 'value',
    'level_one' => [
        'level_two' => [
            'level_three' => [
                'replace_this_array' => [
                    'special_key' => 'replacement_value',
                    'key_one' => 'replacement_value',
                    'key_two' => 'replacement_value',
                    'four' => 'replacement_value',
                    'new_key' => 'new value'
                ]
            ],
            'ordinary_key' => 'value'
        ]
    ]
];
person John Joseph    schedule 08.11.2016

Похоже, что getInnerIterator создает копию вложенного итератора.

Может есть другой метод? (Следите за обновлениями..)


Обновление: после некоторого взлома и привлечения 3 других инженеров не похоже, что PHP дает вам возможность изменить значения subIterator.

Вы всегда можете использовать старый режим ожидания:

<?php  
// Easy to read, if you don't mind references (and runs 3x slower in my tests) 
foreach($aNestedArray as &$subArray) {
    foreach($subArray as &$val) {
       if ($val < 200) {
            $val = 0;
        }
    }
}
?>

OR

<?php 
// Harder to read, but avoids references and is faster.
$outherKeys = array_keys($aNestedArray);
foreach($outherKeys as $outerKey) {
    $innerKeys = array_keys($aNestedArray[$outerKey]);
    foreach($innerKeys as $innerKey) {
        if ($aNestedArray[$outerKey][$innerKey] < 200) {
            $aNestedArray[$outerKey][$innerKey] = 0;
        }
    }
}
?>
person Lance Rushing    schedule 04.08.2009
comment
Я не думаю, что это так просто, как создание копии getInnerIterator, поскольку $cRecursiveIter->getArrayCopy() в цикле дает измененное значение - person John Carter; 04.08.2009

Сначала преобразуйте массив в объект, и он работает так, как ожидалось.

    $array = [
        'one' => 'One',
        'two' => 'Two',
        'three' => [
            'four' => 'Four',
            'five' => [
                'six' => 'Six',
                'seven' => 'Seven'
            ]
        ]
    ];

    // Convert to object (using whatever method you want)
    $array = json_decode(json_encode($array));

    $iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($array));
    foreach($iterator as $key => $value) {
        $iterator->getInnerIterator()->offsetSet($key, strtoupper($value));
    }

    var_dump($iterator->getArrayCopy());
person spooky    schedule 03.04.2017

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

person Adam Byrtek    schedule 04.08.2009
comment
Вы уверены, что? Документы RecursiveArrayIterator говорят, что этот итератор позволяет сбрасывать и изменять значения и ключи при переборе массивов и объектов.... - person John Carter; 04.08.2009

Может ли дело сводиться к передаче по ссылке или передаче по значению?

Например, попробуйте изменить:

$cArray = new ArrayObject($aNestedArray);

to:

$cArray = new ArrayObject(&$aNestedArray);
person Robert Swisher    schedule 04.08.2009
comment
Нет, это не помогает. Для чего бы это ни стоило, использование значения ссылки foreach также не разрешено для итераторов - т.е. это вызывает фатальную ошибку: foreach($cRecursiveIter as &$iVal) - person John Carter; 05.08.2009