Как изменить ключи и значения массива при использовании 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);
Ответы
Ответ 1
Похоже, что значения в простых массивах не изменяются, потому что они не могут передаваться по ссылке на конструктор ArrayIterator
(RecursiveArrayIterator
наследует его методы offset*()
этого класса, см. Ссылка SPL). Таким образом, все вызовы offsetSet()
работают над копией массива.
Я предполагаю, что они решили избежать call-by-reference, потому что это не имеет особого смысла в объектно-ориентированной среде (то есть при передаче экземпляров 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 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());
Ответ 2
Не использовать классы 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 и памяти.
Ответ 3
Похоже, что getInnerIterator создает копию под-итератора.
Может быть, есть другой метод? (следите за обновлениями..)
Обновление: после взлома на нем какое-то время и вытягивания трех других инженеров, это не похоже на то, что 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;
}
}
}
?>
ИЛИ
<?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;
}
}
}
?>
Ответ 4
Вам нужно позвонить getSubIterator
на текущей глубине, используйте offsetSet
на этой глубине и сделайте то же самое для всех глубин, возвращающихся к дереву.
Это действительно полезно для выполнения безлимитного слияния и замены массивов, массивов или значений в массивах. К сожалению, array_walk_recursive
НЕ будет работать в этом случае, так как эта функция только посещает листовые узлы.. поэтому ключ "replace_this_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'
]
]
];
Ответ 5
Сначала преобразуйте массив в объект, и он работает как ожидалось.
$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());
Ответ 6
Я знаю, что это не отвечает на ваш вопрос напрямую, но это не очень хорошая практика для изменения объекта в процессе итерации при итерации по нему.
Ответ 7
Может ли он перейти к передаче по ссылке vs, проходящей по значению?
Например, попробуйте изменить:
$cArray = new ArrayObject($aNestedArray);
в
$cArray = new ArrayObject(&$aNestedArray);