影响实例方法

时间:2017-10-06 01:57:38

标签: 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]可用的名称和方法名称几乎相同。如果你没有分配给其中一个含糊不清的名字,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?语句仍会引用name
  • 提供的attr_accessor实例方法
  • 当Ruby解析器在name = "Unknown"方法中看到#say_name分配行时,它会考虑在赋值之后对name使用的任何引用局部变量
  • 因此,即使Person在初始化时分配了namename方法的最后一行中引用的#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时提供不同的返回值?

2 个答案:

答案 0 :(得分:3)

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

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

为避免方法歧义,您需要在其前面强制使其成为方法调用:

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

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

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

仅在您尝试区分nil?nil时才需要使用false,如果您选中了三个状态复选框,则可能会出现这种情况,选中,取消选中,或者还没有给出答案。

答案 1 :(得分: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

现在,运行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变量。