Ruby的<opinion> odd </opinion>默认Hash.new([])行为的原因

时间:2016-11-30 21:18:45

标签: ruby

我不认为这需要新的,萌芽的Rubyist很长时间才会陷入尝试使用新数组作为默认值创建哈希的陷阱,但却发现Hash.new([])方法确实存在不按预期行事。 (如果您不确定我在谈论什么,请参阅this Stack Overflow question and selected answer。)

是否有人知道这是否是历史或其他原因,这是默认行为?因为我从未遇到过我想要这种行为的情况。即使是假设Rubyist会想要这种行为的假设理由也不是一件容易的事。

我理解这种方式更加一致,因为它以相同的方式处理可变和不可变的默认值,但对于可变和不可变的不同默认行为似乎并不是很糟糕。默认值。

1 个答案:

答案 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

实际上,唯一可行的方法是每次调用时显式dupclone默认值。但这可能非常有问题。请考虑以下修改:

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现在一样,这很好用。但是如果你每次为同一个文件创建一个新句柄,你就会很快用完(在大多数操作系统下你只能同时打开这么多文件)。同样的问题也适用于网络连接,甚至只占用大量内存的对象。

隐式克隆默认哈希值还存在另一个问题:clonedup 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 ++编程,它仍然让我感到惊讶。