开发者

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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜