2009-08-04 3 views
14

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

Как изменить содержимое массива (значения в этом примере), используя 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); 
+0

Похоже, что это ошибка, вы должны подать на http://bugs.php.net/ – null

ответ

2

Похоже getInnerIterator создает копию из суб-итератора.

Возможно, существует другой способ? (Следите ..)


Update: после взлома у него на некоторое время, и вытягивать в 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; 
     } 
    } 
} 
?> 

ИЛИ

<?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; 
     } 
    } 
} 
?> 
+1

Я не думаю, что это так просто, как getInnerIterator создания копии, так как '$ cRecursiveIter -> getArrayCopy() 'внутри цикла дает модифицированное значение –

0

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

+4

Есть ли Вы уверены в этом? В документах «RecursiveArrayIterator» говорится: «Этот итератор позволяет отменить и изменить значения и ключи при повторении массивов и объектов ...». –

0

Не может ли оно перейти к передаче по ссылке против прохождения по значению?

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

$cArray = new ArrayObject($aNestedArray); 

к:

$cArray = new ArrayObject(&$aNestedArray); 
+0

Нет, это не помогает. Для чего это стоит, использование ссылочного значения foreach также не допускается для итераторов - то есть это приводит к фатальной ошибке: 'foreach ($ cRecursiveIter as & $ iVal)' –

5

Кажется, что значения в равнинных массивы не могут быть изменены, поскольку они не могут быть переданы по ссылке конструктору ArrayIterator (RecursiveArrayIterator наследует свои offset*() методов этого класса, см. SPL Reference). Поэтому все обращения к 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());
3

Не используя итераторы классов (которые кажутся копирование данных на 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 и памяти.

+0

Просто потратил час на эти кровавые классы Итератора SPL - возможно, не в первый раз - когда 'array_walk_recursive' делает трюк отлично и с меньшим количеством экземпляров/LOC. Там есть урок, я думаю ... Во всяком случае, спасибо! –

+0

Кажется, что это связанная ошибка DB: https://bugs.php.net/bug.php?id=68682 – powtac

+0

Это не самое идеальное решение, потому что в рекурсивном режиме вы не можете проверить тип данных элементов , Во-вторых, лучше использовать итератор, потому что это больше. – schellingerht

1

На текущей глубине необходимо позвонить 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' 
     ] 
    ] 
]; 
+0

Не решает проблему для меня. – schellingerht

1

Преобразование массива в объект первый и его работает как и ожидалось ..

$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()); 
+0

Спасибо! Это работает! – schellingerht

Смежные вопросы