Ruby哈希默认值行为

54

我正在学习Ruby Koans,目前遇到第41个练习,应该是这个:

def test_default_value_is_the_same_object
  hash = Hash.new([])

  hash[:one] << "uno"
  hash[:two] << "dos"

  assert_equal ["uno","dos"], hash[:one]
  assert_equal ["uno","dos"], hash[:two]
  assert_equal ["uno","dos"], hash[:three]

  assert_equal true, hash[:one].object_id == hash[:two].object_id
end

它无法理解这种行为,所以我在谷歌上搜索并找到了使用Hash默认值时的奇怪的Ruby行为,例如Hash.new([]) 很好地回答了问题。

因此,我理解了它的工作原理,我的问题是,例如增加的整数作为默认值,在使用期间为什么不会更改?例如:

puts "Text please: "
text = gets.chomp

words = text.split(" ")
frequencies = Hash.new(0)
words.each { |word| frequencies[word] += 1 }

这将接受用户输入并计算每个单词被使用的次数,它能够工作是因为始终使用默认值0。

我有一种感觉与“<<”运算符有关,但我希望得到解释。


我相信我曾经看到 "<< "被称为" scoop ",但可能完全错误。 - Jake Sellers
可能是与::混淆了,有时也称为作用域解析运算符。 - user2398029
1
不,我刚刚检查过了,我看过的教程之一将其称为“铲子”,我记忆有误。我相信正确的名称只是连接运算符,可能应该直接使用它。 - Jake Sellers
它也不是连接运算符。它是位左移运算符,也用作附加运算符(用于容器和流)。连接运算符是 + - Hauleth
我之前写过一篇博客文章,如果有人感兴趣的话 :) - Ulysse BN
3个回答

137
其他答案似乎表明行为差异是由于Integer是不可变的,而Array是可变的。但这是误导性的。差异不在于Ruby的创建者决定使一个不可变,另一个可变。差异在于程序员决定改变一个,而不改变另一个。
问题不在于Array是否可变,而在于你是否改变它。
您可以通过使用Array来获得上面看到的两种行为。请看以下示例:

一个带有变异的默认Array

hsh = Hash.new([])

hsh[:one] << 'one'
hsh[:two] << 'two'

hsh[:nonexistent]
# => ['one', 'two']
# Because we mutated the default value, nonexistent keys return the changed value

hsh
# => {}
# But we never mutated the hash itself, therefore it is still empty!

一个不可变的默认Array


(注:该内容涉及IT技术,下同)
hsh = Hash.new([])

hsh[:one] += ['one']
hsh[:two] += ['two']
# This is syntactic sugar for hsh[:two] = hsh[:two] + ['two']

hsh[:nonexistant]
# => []
# We didn't mutate the default value, it is still an empty array

hsh
# => { :one => ['one'], :two => ['two'] }
# This time, we *did* mutate the hash.

每次使用变异都会创建一个新的、不同的Array
hsh = Hash.new { [] }
# This time, instead of a default *value*, we use a default *block*

hsh[:one] << 'one'
hsh[:two] << 'two'

hsh[:nonexistent]
# => []
# We *did* mutate the default value, but it was a fresh one every time.

hsh
# => {}
# But we never mutated the hash itself, therefore it is still empty!


hsh = Hash.new {|hsh, key| hsh[key] = [] }
# This time, instead of a default *value*, we use a default *block*
# And the block not only *returns* the default value, it also *assigns* it

hsh[:one] << 'one'
hsh[:two] << 'two'

hsh[:nonexistent]
# => []
# We *did* mutate the default value, but it was a fresh one every time.

hsh
# => { :one => ['one'], :two => ['two'], :nonexistent => [] }

对于阅读此答案的其他人,请注意,:nonexistent 只是任何名称...它本来可以是 :foo:bar - nonopolarity
请注意,当使用Hash.new {|hsh, key| hsh[key] = [] }时,每次都是一个新的数组实例,而如果使用Hash.new([]),则在键不存在时每次都是完全相同的数组实例。 - nonopolarity
你们在这里看的是 hsh = Hash.new {|hsh, key| hsh[key] = [] } 的代码。 - Epigene

4
这是因为Ruby中的Array是可变对象,所以可以更改其内部状态,但Fixnum是不可变的。所以当您使用 += 增加值时(假设i是对Fixnum对象的引用):

  1. 获取i引用的对象
  2. 获取它的内部值(命名为 raw_tmp
  3. 创建一个新对象,其内部值为raw_tmp + 1
  4. 将对创建对象的引用分配给i

因此,我们创建了一个新对象,i引用现在指向与初始值不同的东西。

另一方面,当我们使用Array#<<时,它的工作方式如下:

  1. 获取arr引用的对象
  2. 将给定元素附加到其内部状态

因此,您可以看到它简单得多,但这可能会导致某些错误。其中之一就是您在问题中遇到的错误,另一个则是当两者同时尝试附加2个或多个元素时的线程竞争。有时,您可能只会得到其中一些,并且在内存中会有一些垃圾。当您也在数组上使用+=时,您将摆脱这两个问题(或至少最小化影响)。


好的,也感谢Lukasz,我现在明白它被引用而不是像我想的那样被复制了。 :) - Jake Sellers
执行1+=1时不会创建新对象。(查看整数的object_id以了解其工作原理。) - steenslag
@steenslag,我知道。这是无效语句。 - Hauleth

1

文档中可以了解到,设置默认值会有以下行为:

返回默认值,即如果hsh中不存在key,则返回hsh将返回的值。另请参见Hash::new和Hash#default=。

因此,每次frequencies[word]未设置时,该键的值都会被设置为0。

两个代码块之间差异的原因是在Ruby中,数组是可变的,而整数不是。


是的,我对这两个代码块中行为上的明显差异感兴趣。在第一个代码块中,使用后默认值被修改,在第二个代码块中似乎不可变。 - Jake Sellers

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