为什么使用splat运算符的Hash#merge返回的是哈希数组而不是哈希数组?

时间:2019-03-14 18:35:53

标签: arrays ruby hash merge splat

TL; DR

我通过反复试验解决了这个问题,但是我对splat运算符和pp方法如何始终如一地给我提供一个不同于我认为的对象的理解上存在差距。我想了解这个差距,并找到一种更好的方式来合并一系列哈希。我还希望将来能够更有效地调试这种事情。

首先是代码示例和调试步骤。我的半满意解决方案和更详细的问题位于底部。

代码

我正在使用MRI Ruby 2.6.2。给定类Foo,我希望Foo#windows返回合并的哈希。这是该类的最小示例:

class Foo
  attr_reader :windows

  def initialize
    @windows = {}
  end

  def pry
    { pry: "stuff pry\r" }
  end 

  def irb
    { irb: "stuff irb\r" }
  end 

  def windows= *hashes
    @windows.merge! *hashes
  end
end

foo = Foo.new
foo.windows = foo.pry, foo.irb

问题(调试)

但是,尝试分配给foo.windows(或者甚至尝试使用foo.windows= foo.pry, foo.irb来减轻歧义),我从REPL中得到了一个例外:

  

TypeError:没有将数组隐式转换为哈希值

但是,如果我使用单例方法修改实例以捕获*hashes参数的值,则会看到一个哈希数组,可以很好地合并它们。请考虑以下内容:

def foo.windows= *hashes
  pp *hashes
end
foo.windows = foo.pry, foo.irb
#=> [{:pry=>"stuff pry\r"}, {:irb=>"stuff irb\r"}]

{}.merge *[{:pry=>"stuff pry\r"}, {:irb=>"stuff irb\r"}]
#=> {:pry=>"stuff pry\r", :irb=>"stuff irb\r"}

抓住#pp的输出,可以得到预期的效果。但是,当我深入研究时,发现在Hash的额外嵌套上有一些分层:

def foo.windows= *hashes
  pp *hashes.inspect
end
foo.windows = foo.pry, foo.irb
"[[{:pry=>\"stuff pry\\r\"}, {:irb=>\"stuff irb\\r\"}]]"

即使返回值没有显示出来,也有一组额外的方括号导致数组嵌套。我真的不明白它们的来源。

什么有效

因此,无论出于何种原因,我都必须对阵列进行扩音,展平,然后才能合并:

def foo.windows= *hashes
  @windows.merge! *hashes.flatten
end

# The method still returns unexpected results here, but...
foo.windows = foo.pry, foo.irb
#=> [{:pry=>"stuff pry\r"}, {:irb=>"stuff irb\r"}]

# the value stored in the instance variable is finally correct!
foo.windows
#=> {:pry=>"stuff pry\r", :irb=>"stuff irb\r"}

但是为什么?

是的,我设法解决了这个问题。但是,我的问题确实是关于为什么合并哈希不能按预期方式工作,以及多余的嵌套层来自何处。我不期望一个哈希数组,而是一个哈希数组。我的理解是否存在差距,或者这是某种奇怪的边缘情况?

更重要的是,为什么这很难调试?我希望#pp或#inspect向我展示我真正拥有的对象,而不是在我显然具有包含哈希的数组数组时向我展示哈希数组作为返回值。

2 个答案:

答案 0 :(得分:3)

首先,*hashes可能有两种截然不同的含义:

  • def windows=(*hashes)的意思是“按顺序将传递给此方法的所有参数存储到数组hashes中。”
  • @windows.merge! *hashes使用hashes的项作为方法调用merge!的各个参数。

但是,当您使用分配方法时,listing several values automatically creates an array:

  

您可以在分配时通过列出多个值来隐式创建一个数组。

a = 1, 2, 3
p a # prints [1, 2, 3]

因此foo.windows(foo.pry, foo.irb)

def windows(*hashes)
    pp hashes
    @windows.merge! *hashes
end

将按预期打印[{:pry=>"stuff pry\r"}, {:irb=>"stuff irb\r"}]。 但是,由于有了赋值方法,您应该只从定义中删除星号。

def windows=(hashes)
  @windows.merge!(*hashes)
end

答案 1 :(得分:2)

您所缺少的是Ruby parser doesn't allow setter methods with more than one parameter

当您将多个参数传递给设置器时,它们会自动组合到一个数组上(因为a = 1, 2a = [1, 2]的含义相同):

def foo.a=(values)
  pp values
end

foo.a = 1, 2 # [1, 2]

但是,如果定义splat参数,则由于该数组被视为单个参数,因此会发生以下情况:

def foo.a=(*values)
  pp values
end

foo.a = 1, 2 # [[1, 2]]