Why doesn't terse way of defining new hashes in Ruby work (they all refer to same object)
I need to establish a number of Hashes, and I didn't want to list one per line, like this
a = Hash.new
b = Hash.new
I also new that apart from for Fixnums, I could not do this
a = b = Hash.new
because both a and b would reference the same object. What I could to is this
a, b, = Hash.new, Hash.new
if I had a bunch it seemed like I could also do this
a, b = [Hash.new] * 2
this works for strings, but for Hashes, they still all reference the same object, despite the fact that
[Hash.new, Hash.new] == [Hash.new] * 2
and the former works.
See the code sample below, the only error message triggered is "multiplication hash broken". Just curious why this is.
a, b, c = [String.new] * 3
a = "hi"
puts "string broken" unless b == ""
puts "not equivalent" unless [Hash.new, Hash.new, Hash.new] == [Hash.new] * 3
a, b, c = [Hash.new, Hash.new, Hash.new]
a['hi'] = :test
puts "normal hash broken" unless b == {}
a, b, c = [Hash.new] * 3
a['hi'] = :test
puts "multiplication hash broken" unless b == {}
开发者_如何学编程
In answer to the original question, an easy way to initialize multiple copies would be to use the Array.new(size) {|index| block }
variant of Array.new
a, b = Array.new(2) { Hash.new }
a, b, c = Array.new(3) { Hash.new }
# ... and so on
On a side note, in addition to the assignment mix-up, the other seeming issue with the original is that it appears you might be making the mistake that ==
is comparing object references of Hash
and String
. Just to be clear, it doesn't.
# Hashes are considered equivalent if they have the same keys/values (or none at all)
hash1, hash2 = {}, {}
hash1 == hash1 #=> true
hash1 == hash2 #=> true
# so of course
[Hash.new, Hash.new] == [Hash.new] * 2 #=> true
# however
different_hashes = [Hash.new, Hash.new]
same_hash_twice = [Hash.new] * 2
different_hashes == same_hash_twice #=> true
different_hashes.map(&:object_id) == same_hash_twice.map(&:object_id) #=> false
My understanding is this. [String.new] * 3
does not create three String
objects. It creates one, and creates a 3-element array where each element points to that same object.
The reason you don't see "string broken" is that you have assigned a
to a new value. So after the line a = "hi"
, a
refers to a new String
object ("hi") while b
and c
still refer to the same original object ("").
The same occurs with [Hash.new] * 3
; but this time you don't re-assign any variables. Rather, you modify the one Hash
object by adding the key/value [hi, :test]
(via a['hi'] = :test
). In this step you've modified the one object referred to by a
, b
, and c
.
Here's a contrived code example to make this more concrete:
class Thing
attr_accessor :value
def initialize(value)
@value = value
end
end
# a, b, and c all refer to the same Thing object
a, b, c = [Thing.new(0)] * 3
# Here we *modify* that object
a.value = 5
# Verify b refers to the same object as a -- outputs "5"
puts b.value
# Now *assign* a to a NEW Thing object
a = Thing.new(10)
# Verify a and b now refer to different objects -- outputs "10, 5"
puts "#{a.value}, #{b.value}"
Does that make sense?
Update: I'm no Ruby guru, so there might be a more common-sense way to do this. But if you wanted to be able to use multiplication-like syntax to initialize an array with a bunch of different objects, you might consider this approach: create an array of lambdas, then call all of them using map
.
Here's what I mean:
def call_all(lambdas)
lambdas.map{ |f| f.call }
end
a, b, c = call_all([lambda{Hash.new}] * 3)
You can verify that this approach works pretty easily:
x, y, z = call_all([lambda{rand(100)}] * 3)
# This should output 3 random (probably different) numbers
puts "#{x}, #{y}, #{z}"
Update 2: I like numbers1311407's approach using Array#new
a lot better.
精彩评论