隐藏实例方法的 Ruby 局部变量的行为
Behaviours of a Ruby local variable shadowing an instance method
我最近看了一篇blog post about Ruby's behaviours with regards to a local variable shadowing a method (different to, say, a block variable shadowing a method local variable, which is also talked about in this Whosebug 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.
所以,给出下面的例子class
# 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
不,与以前相同的 return 值。那么,这是怎么回事?
- 我关于
name.nil?
引用 name
实例方法的假设有误吗? 它引用的是什么?
- 这一切是否与 Pry 环境有关?
- 我还遗漏了什么?
供参考:
➜ [ruby]$ ruby -v
ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-darwin16]
编辑
- 这个问题中的示例代码旨在说明我所看到的(我认为)意外行为,而不是以任何方式说明实际的好代码。
- 我知道通过将局部变量重命名为其他名称可以轻松避免此阴影问题。
- 即使有阴影,我知道仍然可以通过专门调用方法来避免问题,而不是引用局部变量,
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
此时,p
语句还没有运行,那么我们看看Pry说的name
的值是:
[1] pry(#<Person>)> name
nil
这是出乎意料的,因为 Ruby 的文档说由于尚未进行赋值,因此应该调用方法调用。现在让 p
声明 运行...
[2] pry(#<Person>)> next
"Paul"
...方法 name
的值是 returned,这是预期的。
那么,Pry 在这里看到了什么?它是否以某种方式修改范围?为什么当 Pry 运行s name
时它给出不同的 return 值当 Ruby 本身 运行s name
?
一旦 Ruby 确定 name
是一个变量而不是方法调用,该信息将应用于它出现在其中的整个范围。在这种情况下,它意味着整个方法。问题是如果你有一个方法和一个同名的变量,这个变量似乎只在变量被可能赋值的那一行保持不变,这种重新解释会影响所有该方法中的后续行。
与其他语言不同,在其他语言中,方法调用通过某种前缀、后缀或其他指示符变得清晰,在 Ruby name
中,变量和 name
方法调用看起来代码相同,唯一的区别是它们在执行前 "compile" 时间的解释方式。
所以这里发生的事情有点混乱和微妙,但你可以看到 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 中只有两个逻辑错误的值,文字 nil
和 false
。其他一切,包括空字符串、0
、空数组和散列,或 any 类型的对象在逻辑上都是正确的。这意味着除非有机会 name
作为文字 false
有效,否则 ||
可以作为默认值。
只有当你试图区分nil
和false
时才需要使用nil?
,如果你有一个三态复选框,可能会出现这种情况,选中,未选中,或尚未给出答案。
name
在运行时和调试期间看起来不一致的 return 值似乎与 Pry 无关,但更多关于 binding
本身封装 entire 一个方法的执行上下文,相对于在运行时引用的阴影变量的渐进变化。要使用更多调试代码构建示例方法:
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
变量。
我最近看了一篇blog post about Ruby's behaviours with regards to a local variable shadowing a method (different to, say, a block variable shadowing a method local variable, which is also talked about in this Whosebug 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.
所以,给出下面的例子class
# 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
提供的 - 当 Ruby 解析器在
#say_name
方法中看到name = "Unknown"
分配行时,它将考虑在 之后使用的任何对name
的引用 引用局部变量的赋值 - 因此,即使
Person
在初始化时分配了name
,#say_name
方法最后一行中引用的name
也将是nil
name
实例方法
而且 看起来像 这可以在 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
不,与以前相同的 return 值。那么,这是怎么回事?
- 我关于
name.nil?
引用name
实例方法的假设有误吗? 它引用的是什么? - 这一切是否与 Pry 环境有关?
- 我还遗漏了什么?
供参考:
➜ [ruby]$ ruby -v
ruby 2.4.2p198 (2017-09-14 revision 59899) [x86_64-darwin16]
编辑
- 这个问题中的示例代码旨在说明我所看到的(我认为)意外行为,而不是以任何方式说明实际的好代码。
- 我知道通过将局部变量重命名为其他名称可以轻松避免此阴影问题。
- 即使有阴影,我知道仍然可以通过专门调用方法来避免问题,而不是引用局部变量,
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
此时,p
语句还没有运行,那么我们看看Pry说的name
的值是:
[1] pry(#<Person>)> name
nil
这是出乎意料的,因为 Ruby 的文档说由于尚未进行赋值,因此应该调用方法调用。现在让 p
声明 运行...
[2] pry(#<Person>)> next
"Paul"
...方法 name
的值是 returned,这是预期的。
那么,Pry 在这里看到了什么?它是否以某种方式修改范围?为什么当 Pry 运行s name
时它给出不同的 return 值当 Ruby 本身 运行s name
?
一旦 Ruby 确定 name
是一个变量而不是方法调用,该信息将应用于它出现在其中的整个范围。在这种情况下,它意味着整个方法。问题是如果你有一个方法和一个同名的变量,这个变量似乎只在变量被可能赋值的那一行保持不变,这种重新解释会影响所有该方法中的后续行。
与其他语言不同,在其他语言中,方法调用通过某种前缀、后缀或其他指示符变得清晰,在 Ruby name
中,变量和 name
方法调用看起来代码相同,唯一的区别是它们在执行前 "compile" 时间的解释方式。
所以这里发生的事情有点混乱和微妙,但你可以看到 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 中只有两个逻辑错误的值,文字 nil
和 false
。其他一切,包括空字符串、0
、空数组和散列,或 any 类型的对象在逻辑上都是正确的。这意味着除非有机会 name
作为文字 false
有效,否则 ||
可以作为默认值。
只有当你试图区分nil
和false
时才需要使用nil?
,如果你有一个三态复选框,可能会出现这种情况,选中,未选中,或尚未给出答案。
name
在运行时和调试期间看起来不一致的 return 值似乎与 Pry 无关,但更多关于 binding
本身封装 entire 一个方法的执行上下文,相对于在运行时引用的阴影变量的渐进变化。要使用更多调试代码构建示例方法:
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
变量。