你可以写以下。
arr1 = [1, 2, 3, 4]
arr2 = ['a', 'b', 'a', 'c']
arr1.zip(arr2).each_with_object(Hash.new { |h,k| h[k] = [] }) do |(n,c),h|
h[c] << n
end
#=> {"a"=>[1, 3], "b"=>[2], "c"=>[4]}
让我从一个简单的过程方法开始解释这个表达式,然后通过几个步骤来改进代码。
首先创建一个空哈希,它将成为您想要的返回值:
h = {}
我们可以这样写
(0..arr1.size - 1).each do |i|
n = arr1[i]
c = arr2[i]
h[c] = [] unless h.key?(c)
h[c] << n
end
h #=>{"a"=>[1, 3], "b"=>[2], "c"=>[4]}
它更像 Ruby,不过它可以迭代来自 arr1 和 arr2 的对应值对,即 [1, 'a']、[2, 'b'] 等。为此我们使用Array#zip的方法:
pairs = arr1.zip(arr2)
#=> [[1, "a"], [2, "b"], [3, "a"], [4, "c"]]
然后
h = {}
pairs.each do |pair|
n = pair.first
c = pair.last
h[c] = [] unless h.key?(c)
h[c] << n
end
h #=> {"a"=>[1, 3], "b"=>[2], "c"=>[4]}
我们可以做的一个小改进是将array decomposition 应用于pair:
h = {}
pairs.each do |n,c|
h[c] = [] unless h.key?(c)
h[c] << n
end
h #=> {"a"=>[1, 3], "b"=>[2], "c"=>[4]}
接下来的改进是将each替换为Enumerable#each_with_object,以避免需要h = {}开头和h结尾:
pairs.each_with_object({}) do |(n,c),h|
h[c] = [] unless h.key?(c)
h[c] << n
end
#=> {"a"=>[1, 3], "b"=>[2], "c"=>[4]}
注意我是如何编写块变量的,h 持有返回的 object(最初为空的哈希)。这是数组分解的另一种用法。有关该主题的更多信息,请参阅this 文章。
前面的表达式很好,读起来也很好,但是经常看到下面的调整:
pairs.each_with_object({}) do |(n,c),h|
(h[c] ||= []) << n
end
#=> {"a"=>[1, 3], "b"=>[2], "c"=>[4]}
如果h没有密钥c,h[c]返回nil,所以h[c] ||= [],或者h[c] = h[c] || [],变成h[c] = nil || [],所以h[c] = [],之后h[c] << n执行。
没有比前面的表达更好或更差,你可能还看到我在开头提供的代码:
arr1.zip(arr2).each_with_object(Hash.new { |h,k| h[k] = [] }) do |(n,c),h|
h[c] << n
end
这里块变量h被初始化为定义的空哈希
h = Hash.new { |h,k| h[k] = [] }
这采用了Hash::new 的形式,它接受一个块并且没有参数。当这样定义一个哈希h时,如果h没有keyc,执行h[c]会导致h[c] = []在h[c] << n执行之前被执行。