有没有办法在纯 PHP 中检测循环数组?

Posted

技术标签:

【中文标题】有没有办法在纯 PHP 中检测循环数组?【英文标题】:Is there a way to detect circular arrays in pure PHP? 【发布时间】:2021-12-21 08:54:53 【问题描述】:

我正在尝试在 php 中实现我自己的序列化/var_dump 样式函数。如果有循环数组的可能性(确实存在),这似乎是不可能的。

在最近的 PHP 版本中,var_dump 似乎可以检测到循环数组:

php > $a = array();
php > $a[] = &$a;
php > var_dump($a);
array(1) 
  [0]=>
  &array(1) 
    [0]=>
    *RECURSION*
  

我将如何在 PHP 中实现我自己的序列化类型的方法来进行类似的检测?我不能只跟踪我访问过的数组,因为 PHP 中数组的严格比较对于包含相同元素的不同数组返回 true,并且比较循环数组会导致致命错误,无论如何。

php > $b = array(1,2);
php > $c = array(1,2);
php > var_dump($b === $c);
bool(true)
php > $a = array();
php > $a[] = &$a;
php > var_dump($a === $a);
PHP Fatal error:  Nesting level too deep - recursive dependency? in php shell code on line 1

我一直在寻找一种方法来查找数组的唯一 ID(指针),但我找不到。 spl_object_hash 仅适用于对象,不适用于数组。如果我将多个 不同 数组转换为对象,它们都会得到相同的 spl_object_hash 值(为什么?)。

编辑:

在每个数组上调用 print_r、var_dump 或序列化,然后使用某种机制来检测这些方法检测到的递归的存在是算法复杂性的噩梦,并且基本上会使任何使用都太慢而无法在大型嵌套数组上实用。

接受的答案:

我接受了下面的答案,这是第一个建议临时更改一个数组以查看它是否确实与另一个数组相同的答案。这回答了“我如何比较两个数组的身份?”从中递归检测是微不足道的。

【问题讨论】:

答案是:你不能。见check if object/array is a reference。不可能进行类似指针的引用比较,因此也不可能检测到循环。在您的情况下,更好的解决方法可能是通过本机函数之一 (json_decode(json_encode())) 将其转换为摆脱引用,然后才应用您自己的序列化。 现在甚至 PHPUnit 都在使用临时“标记”方法来检测数组递归。 相关Bug #55564 【参考方案1】:

它并不优雅,但可以解决您的问题(至少如果您没有人使用 *RECURSION* 作为值)。

<?php
$a[] = &$a;
if(strpos(print_r($a,1),'*RECURSION*') !== FALSE) echo 1;

【讨论】:

如果你碰巧在你的数组中的任何地方都有字符串"*RECURSION*",那么它就不起作用了......诚然很少见,但可能。 是的,这就是我所说的 *RECURSION* 作为一个值时的意思。无论如何,您可以为这种情况添加一个检查,迭代数组并检查该值,并且根据 print_r 输出给您的行数来限制迭代。 嗯,检测(以一种极其低效的方式)存在递归 somewhere 也许......并不能真正解决我的用例。 正如马里奥之前所说,没有办法检测引用,print_r 正在使用你无权访问的内部,它效率低下,但尽你所能:(【参考方案2】:

有趣的方法(我知道这很愚蠢:)),但您可以修改它并跟踪递归元素的“路径”。这只是一个想法:) 基于序列化字符串的属性,当递归开始时将与原始数组的字符串相同。正如您所看到的 - 我在许多不同的变体上进行了尝试,可能有些东西能够“愚弄”它,但它“检测”了所有列出的递归。而且我没有尝试使用对象的递归数组。

$a = array('b1'=>'a1','b2'=>'a2','b4'=>'a3','b5'=>'R:1;');
$a['a1'] = &$a;
$a['b6'] = &$a;
$a['b6'][] = array(1,2,&$a);
$b = serialize($a); 
print_r($a);
function WalkArrayRecursive(&$array_name, &$temp)
    if (is_array($array_name))
        foreach ($array_name as $k => &$v)
           if (is_array($v))
                if (strpos($temp, preg_replace('#R:\d+;\+$#', '', 
                               serialize($v)))===0) 
                 
                  echo "\n Recursion detected at " . $k ."\n"; 
                  continue; 
                
                WalkArrayRecursive($v, $temp);
            
        
    

WalkArrayRecursive($a, $b);

regexp 适用于具有递归的元素位于数组“末尾”的情况。是的,这个递归与整个数组有关。可以对子元素进行递归,但我想它们为时已晚。不知何故,应该检查数组的每个元素在其子元素中的递归。与上面一样,通过 print_r 函数的输出,或在序列化字符串中查找特定记录以进行递归(R:4; 类似这样)。跟踪应该从那个元素开始,通过我的脚本比较下面的所有内容。只有当你想检测递归从哪里开始时,而不仅仅是你是否拥有它。

ps:但我认为最好的办法应该是从 php 本身创建的序列化字符串编写自己的反序列化函数。

【讨论】:

【参考方案3】:

下面的 isRecursiveArray(array) 方法检测循环/递归数组。它通过临时将包含已知对象引用的元素添加到数组末尾来跟踪已访问的数组。

如果您需要帮助编写序列化方法,请更新您的主题问题并在您的问题中提供示例序列化格式。

function removeLastElementIfSame(array & $array, $reference) 
    if(end($array) === $reference) 
        unset($array[key($array)]);
    


function isRecursiveArrayIteration(array & $array, $reference) 
    $last_element   = end($array);
    if($reference === $last_element) 
        return true;
    
    $array[]    = $reference;

    foreach($array as &$element) 
        if(is_array($element)) 
            if(isRecursiveArrayIteration($element, $reference)) 
                removeLastElementIfSame($array, $reference);
                return true;
            
        
    

    removeLastElementIfSame($array, $reference);

    return false;


function isRecursiveArray(array $array) 
    $some_reference = new stdclass();
    return isRecursiveArrayIteration($array, $some_reference);




$array      = array('a','b','c');
var_dump(isRecursiveArray($array));
print_r($array);



$array      = array('a','b','c');
$array[]    = $array;
var_dump(isRecursiveArray($array));
print_r($array);



$array      = array('a','b','c');
$array[]    = &$array;
var_dump(isRecursiveArray($array));
print_r($array);



$array      = array('a','b','c');
$array[]    = &$array;
$array      = array($array);
var_dump(isRecursiveArray($array));
print_r($array);

【讨论】:

我认为临时更改数组可能是测试两个数组是否相同的唯一合理方法。这是建议这种方法的第一个答案,所以我会接受它。 如果引用它的变量丢失,是否也能检测到循环引用?例如,这会导致无限循环(直到内存耗尽):$array = [[&amp;$array]]; $copy = $array; unset($array); isRecursiveArray($copy);【参考方案4】:

我的方法是使用一个临时数组来保存所有已迭代对象的副本。像这里这样:

// We use this to detect recursion.
global $recursion;
$recursion = [];

function dump( $data, $label, $level = 0 ) 
    global $recursion;

    // Some nice output for debugging/testing...
    echo "\n";
    echo str_repeat( "  ", $level );
    echo $label . " (" . gettype( $data ) . ") ";

    // -- start of our recursion detection logic
    if ( is_object( $data ) ) 
        foreach ( $recursion as $done ) 
            if ( $done === $data ) 
                echo "*RECURSION*";
                return;
            
        

        // This is the key-line: Remember that we processed this item!
        $recursion[] = $data;
    
    // -- end of recursion check

    if ( is_array( $data ) || is_object( $data ) ) 
        foreach ( (array) $data as $key => $item ) 
            dump( $item, $key, $level + 1 );
        
     else 
        echo "= " . $data;
    

这里有一些快速演示代码来说明它是如何工作的:

$obj = new StdClass();
$obj->arr = [];
$obj->arr[] = 'Foo';
$obj->arr[] = $obj;
$obj->arr[] = 'Bar';
$obj->final = 12345;
$obj->a2 = $obj->arr;

dump( $obj, 'obj' );

此脚本将生成以下输出:

obj (object) 
  arr (array) 
    0 (string) = Foo
    1 (object) *RECURSION*
    2 (string) = Bar
  final (integer) = 12345
  a2 (array) 
    0 (string) = Foo
    1 (object) *RECURSION*
    2 (string) = Bar

【讨论】:

问题是关于循环数组,而不是对象。如果将这样的数组传递给您的dump(),它将无限循环。【参考方案5】:

这是我的方法。关键是通过引用递归函数 simple_var_dump() 来传递数组,并使用标记(在本例中为“iterating_in_a_higher_level”)来区分正在更高嵌套级别迭代的数组。

#!/usr/bin/php
<?php

   function simple_var_dump(&$var, $depth = 0)
   
      if (!is_array($var)) 
         if (is_scalar($var)) 
            return (string)$var;
          else 
            return '?';
         
      
      if (isset($var['__iterating_in_a_higher_level__'])) 
         $r = 'array(' . (count($var)-1) . ')';
         return $r . ' *RECURSION*';
      
      $r = 'array(' . count($var) . ')';
      $var['__iterating_in_a_higher_level__'] = true;
      foreach ($var as $key => &$value) 
         if ($key !== '__iterating_in_a_higher_level__') 
            $r .= "\n" . str_repeat('  ', $depth + 1) . '[' . $key . '] => ' . simple_var_dump($value, $depth + 1);
         
      
      unset($var['__iterating_in_a_higher_level__']);
      return $r;
   

   // example:
   //
   $a = [new stdClass(), &$a, 30, [40, [[&$a]]], [1, true, &$a], []];
   echo simple_var_dump($a) . "\n";

输出:

array(6)
  [0] => ?
  [1] => array(6) *RECURSION*
  [2] => 30
  [3] => array(2)
    [0] => 40
    [1] => array(1)
      [0] => array(1)
        [0] => array(6) *RECURSION*
  [4] => array(3)
    [0] => 1
    [1] => 1
    [2] => array(6) *RECURSION*
  [5] => array(0)

【讨论】:

以上是关于有没有办法在纯 PHP 中检测循环数组?的主要内容,如果未能解决你的问题,请参考以下文章

PHP中有没有办法在foreach中重新启动循环,或者在开关中更改测试值?

php [php:foreach的第一个和最后一个]想要在foreach循环中检测数组的第一个和最后一个。 #PHP

在 PHP 中使用 foreach 循环时查找数组的最后一个元素

有没有办法在 C++ 中创建一个循环数组

为啥在 JavaScript 中显示数组元素的上限为 100,有没有办法在没有循环或 splice() 的情况下显示所有内容? [复制]

有没有办法在不使用pairs()的情况下循环遍历数组?