在 Ruby 中,如何从数组生成散列?

我有一个简单的数组:

arr = ["apples", "bananas", "coconuts", "watermelons"]

我还有一个函数 f,它将对单个字符串输入执行操作并返回一个值。这个操作非常昂贵,因此我希望在散列中记录结果。

我知道我可以用这样的东西做出想要的杂烩:

h = {}
arr.each { |a| h[a] = f(a) }

我想要做的是不必初始化 h,这样我就可以写下这样的代码:

h = arr.(???) { |a| a => f(a) }

可以吗?

85136 次浏览

Say you have a function with a funtastic name: "f"

def f(fruit)
fruit + "!"
end


arr = ["apples", "bananas", "coconuts", "watermelons"]
h = Hash[ *arr.collect { |v| [ v, f(v) ] }.flatten ]

will give you:

{"watermelons"=>"watermelons!", "bananas"=>"bananas!", "apples"=>"apples!", "coconuts"=>"coconuts!"}

Updated:

As mentioned in the comments, Ruby 1.8.7 introduces a nicer syntax for this:

h = Hash[arr.collect { |v| [v, f(v)] }]

This is what I would probably write:

h = Hash[arr.zip(arr.map(&method(:f)))]

Simple, clear, obvious, declarative. What more could you want?

h = arr.each_with_object({}) { |v,h| h[v] = f(v) }

I'm doing it like described in this great article http://robots.thoughtbot.com/iteration-as-an-anti-pattern#build-a-hash-from-an-array

array = ["apples", "bananas", "coconuts", "watermelons"]
hash = array.inject({}) { |h,fruit| h.merge(fruit => f(fruit)) }

More info about inject method: http://ruby-doc.org/core-2.0.0/Enumerable.html#method-i-inject

Another one, slightly clearer IMHO -

Hash[*array.reduce([]) { |memo, fruit| memo << fruit << f(fruit) }]

Using length as f() -

2.1.5 :026 > array = ["apples", "bananas", "coconuts", "watermelons"]
=> ["apples", "bananas", "coconuts", "watermelons"]
2.1.5 :027 > Hash[*array.reduce([]) { |memo, fruit| memo << fruit << fruit.length }]
=> {"apples"=>6, "bananas"=>7, "coconuts"=>8, "watermelons"=>11}
2.1.5 :028 >

Did some quick, dirty benchmarks on some of the given answers. (These findings may not be exactly identical with yours based on Ruby version, weird caching, etc. but the general results will be similar.)

arr is a collection of ActiveRecord objects.

Benchmark.measure {
100000.times {
Hash[arr.map{ |a| [a.id, a] }]
}
}

Benchmark @real=0.860651, @cstime=0.0, @cutime=0.0, @stime=0.0, @utime=0.8500000000000005, @total=0.8500000000000005

Benchmark.measure {
100000.times {
h = Hash[arr.collect { |v| [v.id, v] }]
}
}

Benchmark @real=0.74612, @cstime=0.0, @cutime=0.0, @stime=0.010000000000000009, @utime=0.740000000000002, @total=0.750000000000002

Benchmark.measure {
100000.times {
hash = {}
arr.each { |a| hash[a.id] = a }
}
}

Benchmark @real=0.627355, @cstime=0.0, @cutime=0.0, @stime=0.010000000000000009, @utime=0.6199999999999974, @total=0.6299999999999975

Benchmark.measure {
100000.times {
arr.each_with_object({}) { |v, h| h[v.id] = v }
}
}

Benchmark @real=1.650568, @cstime=0.0, @cutime=0.0, @stime=0.12999999999999998, @utime=1.51, @total=1.64

In conclusion

Just because Ruby is expressive and dynamic, doesn't mean you should always go for the prettiest solution. The basic each loop was the fastest in creating a hash.

Ruby 2.6.0 enables a shorter syntax by passing a block to the to_h method:

arr.to_h { |a| [a, f(a)] }

in addition to the answer of Vlado Cingel (I cannot add a comment yet, so I added an answer).

Inject can also be used in this way: the block has to return the accumulator. Only the assignment in the block returns the value of the assignment, and an error is reported.

array = ["apples", "bananas", "coconuts", "watermelons"]
hash = array.inject({}) { |h,fruit| h[fruit]= f(fruit); h }