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

标签 ruby pry shadowing

我最近读了一篇 blog post about Ruby's behaviours with regards to a local variable shadowing a method (与 a block variable shadowing a method local variable 不同,这也在 this StackOverflow thread 中讨论过),我发现了一些我不太理解的行为。

Ruby's documentation says that :

[V]ariable names and method names are nearly identical. If you have not assigned to one of these ambiguous names ruby will assume you wish to call a method. Once you have assigned to the name ruby will assume you wish to reference a local variable.

因此,给定以下示例类

# 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 的最后一行中引用的 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

但是,如果我进行一些内联​​调试并使用 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 实例方法的假设有误吗? 它引用什么?
  • 这一切是否与 Pry 环境有关?
  • 还有什么我错过的吗?

供引用:

➜ [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 在这里看到了什么?它是否以某种方式修改范围?为什么 Pry 运行 name 时会给出与 Ruby 本身运行 name 时不同的返回值?

最佳答案

一旦 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

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

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

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

    puts "My name is #{name.inspect}"
  end

这里要注意的一件事是,在 Ruby 中只有两个逻辑假值,字面值 nilfalse。其他一切,包括空字符串、0、空数组和散列,或任何类型的对象在逻辑上都是正确的。这意味着除非有机会 name 作为文字 false 有效,否则 || 适合默认值。

只有在尝试区分 nilfalse 时才需要使用 nil?,如果您有一个三态复选框,已选中、未选中或尚未给出答案。

关于ruby - 隐藏实例方法的 Ruby 局部变量的行为,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46597191/

相关文章:

ruby-on-rails - 将 attr_encrypted 与客户提供的 key 一起使用(未存储在数据库中)

ruby - binding.pry 和 Pry.start 之间有什么区别?

C 代码与阴影结构声明的混淆行为

javascript - HAML 中动态生成的 Javascript

ruby-on-rails - 如何从 html.erb 调用 JavaScript 函数

ruby-on-rails - ruby rails : Length of Video as a Paperclip Attachment

ruby - Socket 是否与 Pry 捆绑在一起?

bash - 是否可以在 bash 子 shell 中重用只读变量?

java - 如何从父类(super class)中的方法访问(隐藏)子类中的成员变量?