为什么Hash#merge方法使用splat运算符返回的是一个包含多个哈希表的数组,而不是一个哈希表数组?

4

简述

我通过试错法解决了这个问题,但显然我的理解中星号操作符和 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: no implicit conversion of Array into Hash

然而,如果我使用单例方法修改实例以捕获*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 显示我真正拥有的对象,而不是显示一个哈希值数组的返回值,而我明显拥有一个包含哈希值的数组。


你使用的是哪个版本的 Ruby,Todd? - SRack
1
@SRack MRI 2.6.2。 - Todd A. Jacobs
3
“=” 要求一个单一的值;我怀疑 Ruby 正在将你的参数 h1, h2 转换成一个单一的值。星号 “*” 将可变参数移入一个数组中——由于它只接收到一个单一的值,你会看到嵌套的结果。例如,https://dev59.com/vGox5IYBdhLWcg3wVCxy - Dave Newton
1
“pp”不起作用是因为您正在去除星号。 - Dave Newton
2个回答

3

首先,*hashes 可以有两个非常不同的含义:

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

然而,当你有一个赋值方法时,列出多个值会自动创建一个数组:

You can implicitly create an array by listing multiple values when assigning:

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

merge! 的文档显示 varargs 耸肩。你正在链接到 Ruby 2.0;2.6 文档不同。 - Dave Newton
@DaveNewton 噢,似乎是针对2.6+版本的。我的本地2.5版本不支持这个功能。 - idmean
是的,已添加在2.6中。 - Dave Newton

2
你所忽略的是,Ruby解析器不允许具有多个参数的setter方法
当你向setter方法传递多个参数时,它们会自动组合成一个数组(因为a = 1, 2a = [1, 2]意思相同):
def foo.a=(values)
  pp values
end

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

如果您定义了一个星号参数,由于该数组被视为单个参数,因此会发生以下情况: "最初的回答"
def foo.a=(*values)
  pp values
end

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

1
这个回答和idmean的回答都对我有所帮助,但是这个回答更清楚地引起了我的注意,即我正在(滥)使用setter方法语法。一旦我意识到这一点,其他自我造成的问题也消失了。 - Todd A. Jacobs

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