我不认为这需要新的,萌芽的Rubyist很长时间才会陷入尝试使用新数组作为默认值创建哈希的陷阱,但却发现Hash.new([])
方法确实存在不按预期行事。 (如果您不确定我在谈论什么,请参阅this Stack Overflow question and selected answer。)
是否有人知道这是否是历史或其他原因,这是默认行为?因为我从未遇到过我想要这种行为的情况。即使是假设Rubyist会想要这种行为的假设理由也不是一件容易的事。
我理解这种方式更加一致,因为它以相同的方式处理可变和不可变的默认值,但对于可变和不可变的不同默认行为似乎并不是很糟糕。默认值。
答案 0 :(得分:6)
有多个级别可以解释此行为。首先,在直接代码语义级别,您将特定对象作为参数传递给Hash.new
。您说“我希望此对象成为此哈希的默认值。”为什么它会给你一个不同的对象?
考虑这种情况:
default_value = rand(1..10) # random number from 1 to 10
h = Hash.new(default_value)
在这种情况下,很明显你要选择一个随机数,然后该随机数是哈希的默认值。但是,完全与此相同:
h = Hash.new(rand(1..10))
在调用方法之前,总是会计算一次Ruby中的参数。推迟评估的唯一方法是使用块。令人高兴的是,Hash.new
接受了一个块,所以这也适用于默认值:
h = Hash.new { rand(1..10) } # new random value every time
散列的定义大致类似于此(暂时忽略块形式):
class Hash
def initialize(default_value = nil)
@default_value = default_value
@table = HashWithoutDefaults.new
end
def [](key)
if @table.has_key?(key)
@table[key]
else
@default_value
end
end
end
实际上,唯一可行的方法是每次调用时显式dup
或clone
默认值。但这可能非常有问题。请考虑以下修改:
class Hash
def [](key)
if @table.has_key?(key)
@table[key]
else
@default_value.clone
end
end
end
现在想想如何使用它:
logs = Hash.new(File.open('default_log','w'))
logs[:error] = File.open('error_log','w')
logs[:debug] = File.open('debug_log','w')
logs[:debug].puts "Application Launched"
logs[:network].puts "Connecting to server..."
logs[:error].puts "Failed to connect to server"
logs[:network].puts "Retrying connection..."
logs[:network].puts "Successfully connected to server"
logs[:database].puts "Connecting to database"
# ...etc...
正如Ruby现在一样,这很好用。但是如果你每次为同一个文件创建一个新句柄,你就会很快用完(在大多数操作系统下你只能同时打开这么多文件)。同样的问题也适用于网络连接,甚至只占用大量内存的对象。
隐式克隆默认哈希值还存在另一个问题:clone
和dup
produce shallow copies。所以,如果你的默认值是一个字符串数组,那么是的,隐式clone
- ing会让你每次都得到一个新数组,但每个数组中的字符串仍然是相同的字符串;你刚刚把问题推到了一个层面。所以你仍然需要使用块形式:
h = Hash.new(['hello','goodbye']) # using dup-ing Hash
h[:one_key] += ['aloha'] # works how you want now, thanks to dup
h[:another_key].each(&:capitalize!) # whoops!
h[:one_key] # => ['HELLO', 'GOODBYE', 'aloha']
h = Hash.new { ['hello', 'goodbye'] } # works properly
即使不考虑奇怪的边缘案例对象(如File
和深度克隆)的考虑,它只是在语义上更有意义,因为你得到的对象,在很多很多的情况下(基本上任何时候默认值都不是一个空数组或散列)。请考虑以下示例:
joe = Person.new('Joe', job: 'General Contractor', funds: 50)
bob = Person.new('Bob', job: 'Carpenter', funds: 20)
sue = Person.new('Sue', job: 'Painter', funds: 90)
workers = Hash.new(joe) # Joe does all work not done by anyone else
workers[:carpentry] = bob
workers[:painting] = sue
jobs.each do |job|
workers[job.type].pay(job.value)
end
这个代码是否应该在每次工作时都创建一个Joe的副本,并支付副本而不是原始副本,这是否更直观?不是,至少,不是我认为的。
Ruby Hash
的默认值行为可能会让新用户感到困惑,这是绝对正确的。但它完全符合语言其他部分的行为(特别是关于modifying objects passed in to methods)。我认为来自Yukihiro Matsumoto(Ruby的创建者)的引用与此相关:
每个人都有个人背景。有人可能来自Python,其他人可能来自Perl,他们可能会对语言的不同方面感到惊讶。然后他们走到我面前说:“我对这种语言的特性感到惊讶,因此Ruby违反了最少惊喜的原则。”等待。等待。最不出意的原则不仅适用于你。最小惊喜的原则意味着最少我的惊喜的原则。在你非常好地学习Ruby之后,这意味着最不惊讶的原则。例如,在我开始设计Ruby之前,我是一名C ++程序员。我用C ++编程专门用了两三年。经过两年的C ++编程,它仍然让我感到惊讶。