【问题标题】:Why array_unique does not detect duplicate objects?为什么 array_unique 不检测重复对象?
【发布时间】:2020-09-06 16:32:47
【问题描述】:

我似乎无法弄清楚 PHP 场景背后发生了什么魔力,以及为什么 array_unique 无法检测到我的重复项。

在我的具体情况下,我有 2 个用户集合,我将它们合并为一个,然后只保留唯一的条目。为此,我将两个集合都转换为数组,array_merge() 它们,然后基于参数应用 array_unique(..., SORT_REGULAR),以便将它们作为对象进行比较,无需任何转换。我意识到比较对象是一个滑坡,但在这种情况下它比我更奇怪。

在合并之后但在唯一性检查之前我有这个状态:

如您所见,第 4 项和第 11 项是同一个用户实体(非严格比较和严格比较都同意这一点)。然而,在array_unique() 之后,由于某种原因,它们都保留在列表中:

如您所见,项目 7-10 被检测并删除,但 11 没有。

这怎么可能?我在这里没有看到什么?

当前运行 PHP 7.4.5

代码来自使用 Symfony 4.4.7 和 Doctrine ORM 2.7.2 的项目(尽管我认为这应该是无关紧要的,如果对象通过 ===== 比较相等)。

关于奖励积分的有趣事实 - 连续两次应用 array_unique 会产生独特的结果:

头脑 = 炸毁

更新:我在User::__toString() 方法中添加了throw new \RuntimeException(),以确保没有人正在转换为字符串。

请不要建议转换为字符串 - 这既不是我的问题的解决方案,也不是这个问题的意义所在。

【问题讨论】:

  • @u_mulder - 您能否详细说明“仍然不应该为此设置 array_unique”?为什么不呢?
  • array_unique 比较字符串。所以检查比较对象字符串表示(字符串)$a ===(字符串)$b
  • 看起来代码创建了返回值数组(在需要的地方转换为字符串 - 请参阅上转换)并使用第二个数组按索引删除元素以进行比较(代码中的arTmp)。第二个数组使用 pointers 指向变量(请参阅cmpdata->b.val,其中 b 是指针,因此 b.val 不是字符串表示形式)来查找要删除的内容。这有效,因为所有内容都被索引删除。至于您第二次调用该函数,它可以工作,因为这次您传入的是字符串,因为这是第一个函数返回的内容。
  • documentation 中实际上有一个非常明确的警告:“对具有混合类型值的数组进行排序时要小心,因为如果 sort_flags 为 SORT_REGULAR,sort() 会产生意外结果”
  • @Marvin 谢谢,我自己还没有看到这个警告(主要是因为我正在研究 array_unique 并且直到昨天才意识到排序在那里起着重要作用。我会说你的这个评论关于手头整个问题的 50%

标签: php


【解决方案1】:

对于您手头的问题,我真的怀疑这是来自方式 array_unique 正在从数组中删除元素,当使用 SORT_REGULAR 标志时,通过:

  1. sorting it
  2. removing adjacent items if they are equal

因为您的 User 集合中间确实有一个 Proxy 对象,这可能会导致您当前面临的问题。

这似乎得到了sort page of PHP documentation 警告的支持,正如Marvin's comment 所指出的那样。

警告在对具有混合类型值的数组进行排序时要小心,因为如果 sort_flagsSORT_REGULARsort() 会产生意外结果。

来源:https://www.php.net/manual/en/function.sort.php#refsect1-function.sort-notes


现在寻找一个可能的解决方案,这可能会让您获得更多 Symfony 风格的东西。

它使用ArrayCollection filtercontains 方法来过滤第二个集合,只添加第一个集合中不存在的元素。
为了完整起见,这个解决方案还利用了use language construct,以便将第二个ArrayCollection 传递给filter 所需的闭包函数。

这将导致一个新的ArrayCollection 不包含重复的用户。

public static function merge(Collection $a, Collection $b, bool $unique = false): Collection {
  if($unique){
    return new ArrayCollection(
      array_merge(
        $a->toArray(),
        $b->filter(function($item) use ($a){
          return !$a->contains($item);
        })->toArray()
      )
    );
  }

  return new ArrayCollection(array_merge($a->toArray(), $b->toArray()));
}

【讨论】:

  • 谢谢,我会将此作为可接受的答案,因为它最接近我所追求的。这和@Marvin 关于 SORT_REGULAR 排序不同类型对象数组时的警告的评论。在挖掘 PHP 源代码和内部文档时,我了解到 object zval _zend_object_value 以及它是如何由句柄和处理程序表组成的,每个类如何可以有不同的处理程序表,但有些类可以共享一些处理程序,我想sort/array_unique 工作,他们需要共享“比较”处理程序。我只是找不到有关这些处理程序在哪里/什么/哪些的更多信息。
  • 我从一开始就关注这个问题,但也只是在@β.εηοιτ.βε 的 cmets 之后才意识到排序问题。所以......当之无愧的赏金。
【解决方案2】:

我知道您说您不想转换为字符串,但我看到您还没有出路,所以我建议您对数组中的每个对象使用函数serialize,我不'找不到比较未转换为数组或字符串的对象的方法(如果您不熟悉字符串或数组,则不能尝试转换为二进制或十六进制,但我不知道您是否可以转换为二进制或十六进制而不转换为字符串)。

但是,如果你使用serialize,你可以在php自己的读取数据中序列化对象,与其他序列化对象进行比较,这种方法(serialize)是安全的,因为你可以做一个@987654324 @,并再次获取原始对象。

所以你可以序列化数组中的所有元素,然后你可以使用array_unique,就像这样:

<?php

header("Content-Type: application/json");

class MyClass
{
    public $var1;
    public $var2;
    function __construct($var1, $var2)
    {
        $this->var1 = $var1;
        $this->var2 = $var2;
    }

}

$arr = [
    "a",
    "a",
    [1,2,3],
    "b",
    [1,2,3],
    new MyClass(1,1),
    new MyClass(1,new MyClass(1,1)),
    new MyClass(1,new MyClass(1,1)),
];

$arrSerilized = array_map("serialize", $arr);

var_dump(
    array_map(
        "unserialize",
        array_unique(
            $arrSerilized,
            SORT_STRING
        )
    )
);

/* output:
array(5) {
    [0]=>
    string(1) "a"
    [2]=>
    array(3) {
        [0]=>
        int(1)
        [1]=>
        int(2)
        [2]=>
        int(3)
    }
    [3]=>
    string(1) "b"
    [5]=>
    object(MyClass)#6 (2) {
        ["var1"]=>
        int(1)
        ["var2"]=>
        int(1)
    }
    [6]=>
    object(MyClass)#7 (2) {
        ["var1"]=>
        int(1)
        ["var2"]=>
        object(MyClass)#8 (2) {
            ["var1"]=>
            int(1)
            ["var2"]=>
            int(1)
        }
    }
}
*/

希望这对你有所帮助,祝你有美好的一天!

P.S.:使用serialize,您可以在不同的变量类型中保留相同的值,例如1"1"在不同的php读取数据中序列化

【讨论】:

  • 我认为这在 OP 的上下文中不起作用。 Symfony User 是一些特定的东西,它已经具有序列化功能,以便将用户连接到会话中。如果 OP 必须在序列化等会话中创建用户的所有字段,那真的不是很理想
【解决方案3】:

在不了解您的实体类的情况下,很难猜测为什么会发生这种情况。但我想你的主要问题是 __toString() method 。如果您还没有定义它,您应该添加一个,以便它为每个实体对象返回一个唯一/不同的字符串。如果它已经定义,请确保它返回不同的字符串。

class User{ 
   private $name;

   function __construct($name){ 
      $this->name=$name;
   }

   function __toString(){ 
     return $this->name; 
   }
}

$user = [];
$users[] = new User("User1");
$users[] = new User("User2");
$users[] = new User("User3");

$user1= $users[0];
$users[]=$user1; //duplicate

echo(count(array_unique($users))); // output should be 3

鉴于关于实体类的信息有限,我可以猜到这里。

编辑:

阅读您的编辑后,我猜您将自己锁定在此。由于array_unique 将尝试根据您传递的 sort_flag 将实体对象转换为字符串或数字。更多关于array_unique。所以要么你需要实现 __toString() 要么添加一些公共属性来定义你的对象到实体的唯一性,例如

class User{ 
       public $id;
       private $name;

       function __construct($id,$name){
          $this->id=$id;
          $this->name=$name;
       }
}

$user = [];
$users[] = new User(1,"User1");
$users[] = new User(2,"User2");
$users[] = new User(3,"User3");

$user1= $users[0];
$users[]=$user1; //duplicate
echo(count(array_unique($users, SORT_REGULAR))); // output should be 3

请注意公共属性$idSORT_REGULAR 标志。

【讨论】:

  • 您的编辑实际上证明了 OP 的观点,array_unique,当两个项目相同时,使用 SORT_REGULAR 应该可以工作,但由于某种原因它不是
  • @β.εηοιτ.βε 我们可能需要更多关于User 及其公共属性的信息。
  • @sakhunzai 公共属性在这里似乎没有发挥主要作用,因为数组中的两个条目是相同的对象(除非你知道我不知道 sort($items, SORT_REUGAL) 在内部如何工作的东西,尤其是来自 2 个不同类的对象数组)
猜你喜欢
  • 1970-01-01
  • 2021-02-22
  • 1970-01-01
  • 2015-05-05
  • 2011-01-26
  • 2023-03-11
  • 2012-01-16
相关资源
最近更新 更多