Ruby 1.8.6中的Array#uniq不能删除重复的哈希值

3

我在 Ruby 1.8.6 控制台中有这个数组:

arr = [{:foo => "bar"}, {:foo => "bar"}]

两个元素彼此相等:

arr[0] == arr[1]
=> true
#just in case there's some "==" vs "===" oddness...
arr[0] === arr[1]
=> true 

但是,arr.uniq并不会删除重复项:

arr.uniq
=> [{:foo=>"bar"}, {:foo=>"bar"}]

有人能告诉我这里发生了什么吗?

编辑:我可以编写一个不是非常聪明的去重器,使用include?如下:

uniqed = []
arr.each do |hash|
  unless uniqed.include?(hash)
    uniqed << hash
  end
end;false
uniqed
=> [{:foo=>"bar"}]

这将产生正确的结果,使得uniq的失败更加神秘。

编辑2:以下是一些可能仅供我自己理解的说明。正如@Ajedi32在评论中指出的那样,无法去重的原因在于两个元素是不同的对象。一些类定义了eql?hash方法,用于比较,意思是“即使它们不是相同的内存对象,它们是否是实际上相同的东西”。例如,String就是这样做的,这就是为什么你可以定义两个变量为“foo”,它们被认为是相等的,即使它们不是同一个对象。

在Ruby 1.8.6中,Hash类没有这样做,因此当在哈希对象上调用.eql?.hash方法时(.hash方法与Hash数据类型无关 - 它类似于校验和类型的哈希),它会回退到使用Object基类中定义的方法,该方法只是简单地说“它是否是相同的内存对象”。

对于哈希对象,=====运算符已经实现了我想要的功能,即如果它们的内容相同,则两个哈希是相同的。我已经覆盖了Hash#eql?方法来使用它们,如下所示:

class Hash
  def eql?(other_hash)
    self == other_hash
  end
end

但是,我不确定如何处理Hash#hash:也就是说,我不知道如何生成一个校验和,这个校验和对于两个内容相同的哈希表来说是相同的,而对于两个内容不同的哈希表则总是不同的。

@Ajedi32建议我查看Rubinius的Hash#hash方法的实现,链接在此处:https://github.com/rubinius/rubinius/blob/master/core/hash.rb#L589,我的版本的Rubinius实现看起来像这样:

class Hash
  def hash
    result = self.size
    self.each do |key,value|
      result ^= key.hash 
      result ^= value.hash 
    end
    return result
  end
end

这似乎可以工作,但我不知道"^="运算符是什么意思,这让我有点紧张。另外,它非常慢 - 根据一些原始基准测试,大约慢了50倍。这可能使其太慢而无法使用。

编辑3:一些研究表明,“^”是按位异或运算符。当我们有两个输入时,异或运算返回1,如果输入不同(即对于0,0和1,1返回0,并且对于0,1和1,0返回1)。

所以,起初我认为这意味着

result ^= key.hash 

是缩写,代表

result = result ^ key.hash

换句话说,对结果的当前值和另一件事情进行异或运算,然后将其保存在结果中。但我仍然不太理解这个逻辑。我认为^运算符可能与指针有关,因为在变量上调用它可以正常工作,而在变量的值上调用它则无法正常工作:例如
var = 1
=> 1
var ^= :foo
=> 14904
1 ^= :foo
SyntaxError: compile error
(irb):11: syntax error, unexpected tOP_ASGN, expecting $end

因此,在变量上调用 ^= 是可以的,但不能在变量值上调用,这让我想到了引用/取消引用。

Ruby的后续实现还为Hash#hash方法提供了C代码,而Rubinius的实现似乎太慢了。有些困难...


Ruby 1.8.6发布已经超过10年了,9年前更新了1.8.7版本,所有的1.8.x版本在4年前就已经到达了生命周期终点。你为什么还要关心它呢? - spickermann
4
为什么有人关心任何旧版本的东西?因为他们被迫在传统网站上使用这些旧版本。 - Max Williams
不得不与传统应用程序一起工作是一回事,但不得不使用已经过时超过9年的版本的应用程序则更为荒谬。哇,这太可笑了... - spickermann
@spickermann 这里没有任何争议。 - Max Williams
2个回答

2
为了提高效率,Array#uniq 不使用 == 或者 === 来比较值。根据文档所述:

它使用哈希和 eql? 方法来比较值以提高效率。

(请注意,我在此处链接的是 2.4.2 版本的文档。虽然 1.8.6 版本的文档没有包含这个声明,但我认为对于那个版本的 Ruby 它仍然成立。)

在 Ruby 1.8.6 中,Hash#hashHash#eql? 都没有被实现,因此它们会回退到使用Object#hashObject#eql?

相等性——在对象级别上,只有当 obj 和 other 是同一个对象时,== 才返回 true。通常,这个方法会在子类中被重写以提供特定于类的含义。

[...]

eql? 方法如果 obj 和 anObject 具有相同的值,则返回 true。用于 Hash 来测试成员的相等性。对于 Object 类的对象,eql?== 是同义词。

因此,根据 Array#uniq,这两个哈希是不同的对象,因此它们是唯一的。

要解决这个问题,你可以尝试自己定义Hash#hashHash#eql?。如何实现这些方法的细节留给读者自己去完成。但你可能会发现参考Rubinius 的这些方法的实现会有所帮助。


啊 - 是的:arr[0].eql?(arr[1]) 返回 false,所以看起来这就是关键。谢谢! - Max Williams
作为一个额外的问题,您认为定义eql?以使其在哈希中表现正常的最简洁的方法是什么?(即测试内容是否相同)。我想我可以只做self == other_hash,但这感觉有些麻烦... - Max Williams
@MaxWilliams 我可能会检查每个哈希的类和长度是否相同,然后使用 Hash#each_pair 迭代遍历每个键和值,并使用 eql? 进行比较。(虽然我不确定顺序是否应该有所区别?)您可能还需要使用类似的方法来定义 Hash#hash - Ajedi32
实际上,@Ajedi32,我认为.eql?并不是关键。我添加了一个扩展到Hash中,使得eql?方法只需执行self == other_hash,现在我得到了arr[0].eql?(arr[1]) => true。太好了。然而,uniq仍然保留重复项,这让我想到uniq并没有使用eql?。根据我的API文档,uniq的源代码看起来像C代码。 - Max Williams
啊,抱歉,我漏掉了那一部分。Hash#hash,真是令人困惑啊 :) - Max Williams
显示剩余3条评论

0

使用JSON stringify将其转换为字符串,然后像JavaScript一样解析回来怎么样?

require 'json'
arr.map { |x| x.to_json}.uniq.map { |x| JSON.parse(x) }

json 方法可能在 1.8.6 中不受支持,请使用任何被支持的方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接