使用嵌套的 foreach 循环取消设置数组中的对象未按预期工作?

Using nested foreach loops to unset objects in array not working as expected?

我有一组具有 idparentId 属性的对象。 id 值是唯一的,但多个对象可能具有相同的 parentId 值。

如果多个对象具有相同的 parentId 值,我想删除除一个之外的所有对象(即删除 "siblings")。

我以为我可以用嵌套的 foreach 循环轻松地做到这一点,但它并没有像我预期的那样工作。

这是一个例子:

$objArray = [];

for($i = 0; $i < 2; $i++) {
    $obj = new stdClass();
    $obj->id = $i;
    $obj->parentId = 1;

    $objArray[] = $obj;
}

for($i = 2; $i < 4; $i++) {
    $obj = new stdClass();
    $obj->id = $i;
    $obj->parentId = 2;

    $objArray[] = $obj;
}

echo 'Before unsetting siblings:<pre>';
print_r($objArray);
echo '</pre>';

// loop over $objArray and remove elements with the same ->parentId (leaving one)
foreach ($objArray as $keyOuter => $objOuter) {
    foreach ($objArray as $keyInner => $objInner) {
        if ($objInner->id != $objOuter->id             // if the inner object is NOT the same as the outer object (i.e. it's a different object)
         && $objInner->parentId == $objOuter->parentId // and if the parent IDs are the same
        ) {
            unset($objArray[$keyInner]); // unset the inner object
        }
    }
}

echo 'After unsetting siblings:<pre>';
print_r($objArray);
echo '</pre>';

输出:

Before unsetting siblings:

Array
(
    [0] => stdClass Object
        (
            [id] => 0
            [parentId] => 1
        )

    [1] => stdClass Object
        (
            [id] => 1
            [parentId] => 1
        )

    [2] => stdClass Object
        (
            [id] => 2
            [parentId] => 2
        )

    [3] => stdClass Object
        (
            [id] => 3
            [parentId] => 2
        )

)

After unsetting siblings:

Array
(
)

我希望数组中的第一个和第三个对象在 foreach 循环后保留,但正如您所看到的,数组中的所有对象都被删除了。

我在这里错过了什么?

你的 for 循环的前两行是相同的,使用 var_dump 自己确认一下:

foreach ($objArray as $keyOuter => $objOuter) {
    foreach ($objArray as $keyInner => $objInner) { 
        var_dump($objArray, $objInner); //the same object is returned
        ...

所以从技术上讲,objOuter 在第二个 foreach 的第一个 运行 中始终等于 objInner,因此删除了对象,最后您将删除所有对象。

对于这段代码,给你的变量一个不同的名字:

for($i = 2; $i < 4; $i++) {
    $obj = new stdClass();
    $obj->id = $i;
    $obj->parentId = 2;

    $objArray2[] = $obj; //instead of $objArray
}

这是最终代码,它按照您希望的方式工作:

for($i = 0; $i < 2; $i++) {
    $obj = new stdClass();
    $obj->id = $i;
    $obj->parentId = 1;

    $objArray[] = $obj;
}

for($i = 2; $i < 4; $i++) {
    $obj = new stdClass();
    $obj->id = $i;
    $obj->parentId = 2;

    $objArray2[] = $obj;
}

echo 'Before unsetting siblings:<pre>';
print_r($objArray);
echo '</pre>';

foreach ($objArray as $keyOuter => $objOuter) {
    foreach ($objArray2 as $keyInner => $objInner) {
        if ($objInner->id != $objOuter->id 
            && $objInner->parentId == $objOuter->parentId) {
        unset($objArray[$keyInner]); // unset the inner object
        }
    }
}

echo 'After unsetting siblings:<pre>';
print_r($objArray);
echo '</pre>';

我在你的内部循环中添加了这一行:

echo 'Matched id #'.$objOuter->id.' parent #'.$objOuter->parentId.' with id #'.$objInner->id.' parent #'.$objInner->parentId."\r\n";

产生了:

Matched id #0 parent #1 with id #1 parent #1

Matched id #1 parent #1 with id #0 parent #1

Matched id #2 parent #2 with id #3 parent #2

Matched id #3 parent #2 with id #2 parent #2

再演示(!!表示没有match/deletion,==表示父匹配有删除):

0/1!!0/1 0/1==1/1 0/1!!2/2 0/1!!3/2 
1/1==0/1 1/1!!2/2 1/1!!3/2 
2/2!!2/2 2/2==3/2 
3/2==2/2 

看到规律了吗?在您拥有 unset 已匹配的元素后,您的内循环正在获取最新版本的 $objArray,而您的外循环没有新版本,因为 foreach 实际上保留了一个临时的$objOuter$keyOuter 值的克隆数组。

这是一个概念证明:

$array = array(1,2,3,4);

foreach ($array as $a) {
    if (isset($array)) unset($array);
    echo $a;
}

输出:

1234

或者这个:

$array = array(1,2,3,4);

foreach ($array as $a) {
    $array[3] = 100;
    echo $a; // Output is still 1234
}

或者这个:

$array = array(1,2,3,4);

foreach ($array as $a) {
    if (isset($array[3])) unset($array[3]);
    echo $a; // Yet again 1234
}

如果$array不存在了,为什么循环还在继续?按照同样的逻辑,为什么我的第二个例子的输出不是 123100?您的外循环存在相同的 flaw/bug。

我宁愿创建一个新的过滤数组,也不愿尝试使用嵌套循环从原始数组中删除:

$newArray = array();
foreach ($objArray as $obj) {
    if (!isset($newArray[$obj->parentId])) { // Use the index to test for existing parent IDs
        $newArray[$obj->parentId] = $obj;
    }
}
// Optional - use array_values to get rid of parentId in the indices
$newArray = array_values($newArray);
// or you can just do this if you want to replace $objArray
$objArray = array_values($newArray);
unset($newArray);

您还可以保留另一个现有父键数组,如果该键已存在,则从现有数组中删除:

$existingParents = array();
foreach ($objArray as $key => $obj) {
    if (isset($existingParents[$obj->parentId])) {
        unset($objArray[$key]);
    } else {
        $existingParents[$obj->parentId] = true;
    }
}

快速解决方案:

$objArray = [];

for($i = 0; $i < 2; $i++) {
    $obj = new stdClass();
    $obj->id = $i;
    $obj->parentId = 1;

    $objArray[] = $obj;
}

for($i = 2; $i < 4; $i++) {
    $obj = new stdClass();
    $obj->id = $i;
    $obj->parentId = 2;

    $objArray[] = $obj;
}

echo 'Before unsetting siblings:<pre>';
print_r($objArray);
echo '</pre>';

// loop over $objArray and remove elements with the same ->parentId (leaving one)
$max = count($objArray);
for ($i=0; $i<$max; $i++) {
    $objOuter = $objArray[$i];
    foreach ($objArray as $i2=>$objInner) {
        if ($objInner->id != $objOuter->id             // if the inner object is NOT the same as the outer object (i.e. it's a different object)
         && $objInner->parentId == $objOuter->parentId // and if the parent IDs are the same
        ) {
            unset($objArray[$i2]);
            $max=$max-1;// unset the inner object
        }
    }
}

echo 'After unsetting siblings:<pre>';
print_r($objArray);
echo '</pre>';