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

Ruby 1.8.6 Array#uniq not removing duplicate hashes

我有这个数组,在 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"}]

谁能告诉我这是怎么回事?

编辑:我可以写一个不太聪明的 uniqifier,它使用 include? 如下:

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

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

编辑 2:关于正在发生的事情的一些注释,可能只是为了我自己的清楚。正如 @Ajedi32 在评论中指出的那样,统一化失败是因为这两个元素是不同的对象。一些classes定义了eql?hash方法,用于比较,意思是"are these effectively the same thing, even if they're not the same object in memory"。例如,String 就是这样做的,这就是为什么您可以将两个变量定义为 "foo" 并且据说它们彼此相等,即使它们不是同一个对象。

哈希 class 不会 在 Ruby 1.8.6 中执行此操作,因此当 .eql?.hash 在散列对象上调用(.hash 方法与散列数据类型无关 - 它就像散列的校验和类型)它回退到使用对象基 class 中定义的方法,它简单地说 "Is it the same object in memory".

对于散列对象,===== 运算符已经做了我想要的,即如果两个散列的内容相同,则它们是相同的。我已经覆盖 Hash#eql? 来使用这些,如下所示:

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

但是,我不确定如何处理 Hash#hash:也就是说,我不知道如何为两个内容相同但总是不同的散列生成相同的校验和对于具有不同内容的两个哈希值。

@Ajedi32 建议我在这里 https://github.com/rubinius/rubinius/blob/master/core/hash.rb#L589 看看 Rubinius 对 Hash#hash 方法的实现,我的 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:一些研究表明“^”是按位异或运算符。当我们有两个输入时,如果输入不同,则 XOR returns 1(即 returns 0 表示 0,0 和 1,1,1 表示 0,1 和 1,0)。

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

result ^= key.hash 

是 shorthand 对于

result = result ^ key.hash

换句话说,在result的当前值和其他东西之间进行XOR,然后将其保存在result中。不过,我仍然不太明白其中的逻辑。我认为 ^ 运算符可能与指针有关,因为在变量上调用它有效,而在变量值上调用它不起作用:例如

var = 1
=> 1
var ^= :foo
=> 14904
1 ^= :foo
SyntaxError: compile error
(irb):11: syntax error, unexpected tOP_ASGN, expecting $end

所以,在变量上调用 ^= 没问题,但不是变量的值,这让我觉得这与 referencing/dereferencing.

有关

Ruby 的后期实现也有 Hash#hash 方法的 C 代码,Rubinius 的实现似乎太慢了。有点卡住了...

出于效率原因,Array#uniq 不使用 == 甚至 === 比较值。根据 the docs:

It compares values using their hash and eql? methods for efficiency.

(注意我在这里链接了 2.4.2 的文档。虽然 1.8.6 的文档不包含此声明,但我相信它仍然适用于 Ruby 的那个版本。)

在 Ruby 1.8.6 中,neither Hash#hash nor Hash#eql? are implemented, so they fallback to using Object#hash and Object#eql?:

Equality—At the Object level, == returns true only if obj and other are the same object. Typically, this method is overridden in descendent classes to provide class-specific meaning.

[...]

The eql? method returns true if obj and anObject have the same value. Used by Hash to test members for equality. For objects of class Object, eql? is synonymous with ==.

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

要解决此问题,您可以尝试定义 Hash#hash and Hash#eql? yourself. The details of how to do this are left as an exercise to the reader. You may find it helpful however to refer to Rubinius's implementation of these methods.

如何使用 JSON stringify 并像 Javascript 那样对其进行解析?

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

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