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

Pau*_*nti 5 ruby shadowing pry

我最近阅读了一篇关于 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
Run Code Online (Sandbox Code Playgroud)

鉴于我现在通过阅读上述链接中的信息所了解的信息,我希望得到以下信息:

  • name.nil?语句仍将引用name由提供的实例方法attr_accessor
  • 当 Ruby 解析器看到方法中的name = "Unknown"赋值行时#say_name,它会考虑在赋值之后name使用的任何引用来引用局部变量
  • 因此,即使Person有一个name分配给它的初始化时,name在最后一行引用的#say_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
Run Code Online (Sandbox Code Playgroud)

但是,如果我进行一些内联​​调试并使用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"
Run Code Online (Sandbox Code Playgroud)

好的,这是有道理的,因为我假设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
Run Code Online (Sandbox Code Playgroud)

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

[3] pry(#<Person>)> exit
My name is nil
nil
Run Code Online (Sandbox Code Playgroud)

不,与以前相同的返回值。那么,这里发生了什么?

  • 我对name.nil?引用name实例方法的假设是否错误?什么它引用?
  • 所有这些都与处于 Pry 环境有关吗?
  • 我还错过了什么?

以供参考:

? [ruby]$ ruby -v
ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-darwin16]
Run Code Online (Sandbox Code Playgroud)

编辑

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

进一步研究这个问题,我开始认为这可能是 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
Run Code Online (Sandbox Code Playgroud)

此时,p语句还没有运行,所以让我们看看 Pry 说的值name是什么:

[1] pry(#<Person>)> name
nil
Run Code Online (Sandbox Code Playgroud)

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

[2] pry(#<Person>)> next
"Paul"
Run Code Online (Sandbox Code Playgroud)

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

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

tad*_*man 5

一旦 Ruby 确定这name是一个变量而不是方法调用,该信息就会应用于它出现的整个范围。在这种情况下,它是指整个方法。问题是如果你有一个方法和一个同名的变量,这个变量似乎只在变量可能被分配到的行上保持不变,这种重新解释会影响该方法中的所有后续行。

与通过某种前缀、后缀或其他指示符明确方法调用的其他语言不同,在 Ruby 中name,变量和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
Run Code Online (Sandbox Code Playgroud)

我很惊讶,鉴于-w启用该标志的特定 Ruby 是多么令人讨厌,这种特定情况根本不会产生任何警告。这可能是必须发出警告的事情,即带有变量的方法的奇怪部分阴影。

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

  def say_name
    name = self.name || 'Unknown'

    puts "My name is #{name.inspect}"
  end
Run Code Online (Sandbox Code Playgroud)

这里要注意的一件事是,在 Ruby 中只有两个逻辑错误值,字面量nilfalse. 其他所有内容,包括空字符串、0空数组和散列或任何类型的对象在逻辑上都是正确的。这意味着,除非有一个机会,name就是有效的文字false||是违约罚款。

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


Pau*_*nti 0

在运行时和调试期间看起来不一致的返回值似乎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
Run Code Online (Sandbox Code Playgroud)

现在,运行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
Run Code Online (Sandbox Code Playgroud)

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