隐藏实例方法的 Ruby 局部变量的行为



我最近读了一篇关于 Ruby 关于局部变量遮蔽方法的行为的博客文章(不同于,比如说,块变量遮蔽方法局部变量,在这个 StackOverflow 线程中也谈到了这一点),我发现了一些我不太理解的行为。

Ruby的文档说:

[V]可获取的名称和方法名称几乎相同。如果您尚未为这些不明确的名称之一赋值,Ruby 将假定您希望调用一个方法。一旦你分配了这个名字,ruby 就会假设你想引用一个局部变量。

因此,给定以下示例类

# person.rb
class Person
attr_accessor :name
def initialize(name = nil)
@name = name
end
def say_name
if name.nil?
name = "Unknown"
end
puts "My name is #{name.inspect}"
end
end

鉴于我现在从阅读上述链接中了解到的信息,我希望以下内容:

  • name.nil?语句仍将引用attr_accessor提供的name实例方法
  • 当 Ruby 解析器在#say_name方法中看到name = "Unknown"赋值行时,它会考虑对赋值使用的任何name引用来引用局部变量
  • 因此,即使Person在初始化时分配了name#say_name方法的最后一行中引用的name也将nil

看起来这可以在irb控制台中确认:

irb(main):001:0> require "./person.rb"
true
# `name.nil?` using instance method fails,
# `name` local variable not assigned
irb(main):002:0> Person.new("Paul").say_name
My name is nil
nil
# `name.nil?` using instance method succeeds
# as no name given on initialisation,
# `name` local variable gets assigned
irb(main):003:0> Person.new.say_name
My name is "Unknown"
nil

但是,如果我进行一些内联调试并使用 Pry 尝试跟踪name引用如何更改,我会得到以下结果:

irb(main):002:0> Person.new("Paul").say_name
From: /Users/paul/person.rb @ line 13 Person#say_name:
10: def say_name
11:   binding.pry
12:
=> 13:   p name
14:   if name.nil?
15:     name = "Unknown"
16:   end
17:
18:   puts "My name is #{name.inspect}"
19: end
[1] pry(#<Person>)> next
"Paul"

好的,这是有道理的,因为我假设name指的是实例方法。 所以,让我们直接检查name的值...

From: /Users/paul/person.rb @ line 14 Person#say_name:
10: def say_name
11:   binding.pry
12:
13:   p name
=> 14:   if name.nil?
15:     name = "Unknown"
16:   end
17:
18:   puts "My name is #{name.inspect}"
19: end
[2] pry(#<Person>)> name
nil

呃......这在这一点上是出乎意料的。 我目前正在查看对赋值行上方name的引用,所以我本以为它仍然会引用实例方法而不是局部变量,所以现在我很困惑......我想不知何故name = "Unknown"作业会运行,然后...?

[3] pry(#<Person>)> exit
My name is nil
nil

不,返回值与以前相同。那么,这是怎么回事呢?

  • 我对name.nil?引用name实例方法的假设是错误的吗?它指的是什么?
  • 这一切与在撬环境中有关吗?
  • 我还错过了什么?

供参考:

➜ [ruby]$ ruby -v
ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-darwin16]

编辑

  • 这个问题中的示例代码旨在说明我所看到的(我认为)意外行为,而不是以任何方式说明实际的好代码。
  • 我知道通过将局部变量重命名为其他变量很容易避免这种阴影问题。
  • 即使有阴影,我知道仍然可以通过专门调用该方法来避免这个问题,而不是引用局部变量,self.namename()

进一步玩弄这个问题,我开始认为这可能是Pry环境的问题。运行时Person.new("Paul").say_name

From: /Users/paul/person.rb @ line 13 Person#say_name:
10: def say_name
11:   binding.pry
12:
=> 13:   p name
14:   if name.nil?
15:     name = "Unknown"
16:   end
17:
18:   puts "My name is #{name.inspect}"
19: end

此时,p语句尚未运行,因此让我们看看 Pry 所说的name值是什么:

[1] pry(#<Person>)> name
nil

这是出乎意料的,因为 Ruby 的文档说由于尚未进行赋值,因此应该调用方法调用。 现在让我们运行p语句...

[2] pry(#<Person>)> next
"Paul"

。并返回方法name的值,这是预期的。

那么,普瑞在这里看到了什么?它是否以某种方式修改了范围?为什么当 Pry 运行时name它给出的返回值与 Ruby 本身运行时name不同?

一旦 Ruby 确定name是一个变量而不是一个方法调用,该信息将应用于它出现的整体范围。在这种情况下,它意味着整个方法。麻烦的是,如果你有一个方法和一个同名的变量,那么变量似乎只在变量可能被分配到的行上站稳脚跟,并且这种重新解释会影响该方法中的所有后续行。

与其他语言中的方法调用通过某种前缀、后缀或其他指示符明确表示不同,在 Runameby 中,变量和name方法调用在代码中看起来相同,唯一的区别是它们在执行的"编译"时是如何解释的。

所以这里发生的事情有点令人困惑和微妙,但你可以看到name是如何用local_variables来解释的:

def say_name_local_variable
p defined?(name)      # => "method"
p local_variables     # => [:name] so Ruby's aware of the variable already
if name.nil?          # <- Method call
name = "Unknown"    # ** From this point on name refers to the variable
end                   #    even if this block never runs.
p defined?(name)      # => "local-variable"
p name                # <- Variable value
puts "My name is #{name.inspect}"
end

我很惊讶,考虑到启用-w标志的 Ruby 是多么令人讨厌,这种特殊情况根本不会生成任何警告。这可能是必须发出警告的东西,一种带有变量的方法的奇怪部分阴影。

为了避免方法歧义,您需要为其添加前缀以强制它成为方法调用:

def say_name
name = self.name || 'Unknown'
puts "My name is #{name.inspect}"
end

这里要注意的一件事是,在 Ruby 中只有两个逻辑错误的值,文字nilfalse。其他所有内容,包括空字符串、0、空数组和哈希或任何类型的对象,在逻辑上都是正确的。这意味着除非name有可能作为文字false有效,否则||默认值是可以的。

仅当您尝试区分nilfalse时,才需要使用nil?,如果您有一个三态复选框、选中、未选中或尚未给出答案,则可能会出现这种情况。

在运行时和调试期间看起来不一致的name返回值似乎与 Pry 无关,而是更多关于binding本身封装方法的整个执行上下文,而不是在运行时影子变量引用的渐进式更改。若要使用更多调试代码在示例方法的基础上进行构建,请执行以下操作:

def say_name
puts "--- Before assignment of name: ---"
puts "defined?(name) : #{defined?(name).inspect}"
puts "binding.local_variable_defined?(:name) : #{binding.local_variable_defined?(:name).inspect}"
puts "local_variables : #{local_variables.inspect}"
puts "binding.local_variables : #{binding.local_variables.inspect}"
puts "name : #{name.inspect}"
puts "binding.eval('name') : #{binding.eval('name').inspect}"
if name.nil?
name = "Unknown"
end
puts "--- After assignment of name: ---"
puts "defined?(name) : #{defined?(name).inspect}"
puts "binding.local_variable_defined?(:name) : #{binding.local_variable_defined?(:name).inspect}"
puts "local_variables : #{local_variables.inspect}"
puts "binding.local_variables : #{binding.local_variables.inspect}"
puts "name : #{name.inspect}"
puts "binding.eval('name') : #{binding.eval('name').inspect}"
puts "My name is #{name.inspect}"
end

现在,运行Person.new("Paul").say_name输出:

--- Before assignment of name: ---
defined?(name) : "method"
binding.local_variable_defined?(:name) : true
local_variables : [:name]
binding.local_variables : [:name]
name : "Paul"
binding.eval('name') : nil
--- After assignment of name: ---
defined?(name) : "local-variable"
binding.local_variable_defined?(:name) : true
local_variables : [:name]
binding.local_variables : [:name]
name : nil
binding.eval('name') : nil
My name is nil

这表明binding从不引用name的方法调用,只引用最终分配的name变量。

最新更新