【问题标题】:Behaviours of a Ruby local variable shadowing an instance method遮蔽实例方法的 Ruby 局部变量的行为
【发布时间】:2017-10-06 01:57:38
【问题描述】:

我最近阅读了一个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] 变量名和方法名几乎相同。如果您没有指定这些模棱两可的名称之一,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 的引用,所以我认为它仍然会引用实例方法而不是局部变量,所以现在我很困惑......我猜想@987654346 @assignment 会运行,然后...?

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

不,返回值和以前一样。那么,这里发生了什么?

  • 我对@9​​87654348@ 引用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 时不同?

【问题讨论】:

  • 我只想说一个显而易见的问题:使用不同的变量名可以轻松避免整个麻烦,例如display_name = name || "Unknown".
  • 是的,正确的。问题中的代码是按照严格说明我所看到的问题的方式编写的。

标签: ruby pry shadowing


【解决方案1】:

一旦 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、空数组和散列,或 any 类型的对象在逻辑上都是正确的。这意味着除非有机会 name 作为文字 false 有效,否则 || 可以作为默认值。

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

【讨论】:

  • 感谢您的回答。关于Ruby“决定name是一个变量而不是一个方法调用,该信息适用于它出现的范围的整体”(我在某处也读到了关于将变量值应用于范围的整体),在#say_name 方法的范围内似乎不会发生这种情况,否则Person.new("Paul").say_namePerson.new.say_name 会打印出相同的消息(即name.nil? 两次都会失败)。我错过了什么?
  • 这可能是由于范围界定,这可能是 if 本身。为避免范围混淆,最好一开始就避免这样做。我可能会混淆 JavaScript 变量范围规则和 Ruby 的特殊特性,但这里的原理是一样的。
  • 别担心,我完全不提倡做这种事情,因为它会引起混乱。问题中的示例仅用于说明目的 :) 此问题的潜在原因是 if 语句本身的范围很有趣,因此我可能会对此进行研究。
  • 我在此处添加了一些关于如何使用 defined?local_variables 发现内部情况的更多信息。
  • 括号也可以用来避免歧义,即name() || "Undefined"
【解决方案2】:

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 变量。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-19
    • 2011-08-27
    • 2014-03-06
    • 1970-01-01
    • 2011-11-02
    相关资源
    最近更新 更多