我有些惊讶地发现person
是由以下代码定义的,即使params[:person_id]
不存在:
person = Person.find(params[:person_id]) if params[:person_id]
我期望Ruby首先检查if
语句,然后仅定义person
。实际上,person
似乎比这更早定义,但仍然是nil
。
在调查我尝试以下操作时:
irb> foo
# NameError (undefined local variable or method `foo' for main:Object)
irb> if false
irb> foo = 'bar'
irb> end
irb> foo
# => nil
最初foo
是未定义的。但是,即使它仅在未评估的if
块中引用。
我现在猜测整个程序都会被解析(?(,并且将foo
节点添加到抽象语法树(即定义(中。然后执行该程序(?(,但是该系列被跳过(未评估(?((,因此foo是 nil
(定义但未设置为一个值(。
我不确定如何确认或反驳这种预感。一个人如何学习和挖掘红宝石内部内容并找出在这种特定情况下发生的情况?
回答我自己的问题,杰伊对与文档的一部分有关的类似问题的回答:
当解析器遇到分配时,创建本地变量,而不是在分配发生时
在Ruby Hacking指南中对此有更深入的分析(没有可用的部分链接,搜索或滚动到"本地变量定义"部分(:
顺便说一句,当"出现"时,它被定义,这意味着即使未分配也是定义的。定义[但尚未分配]变量的初始值是nil。
回答最初的问题,但不能如何了解更多。
Jay和Simonwo都建议您阅读的Pat Shaughnessy在显微镜下建议Ruby。
此外,其余的Ruby Hacking Guide涵盖了很多细节,并实际检查了基础C代码。对象和解析器章节与有关变量分配的原始问题特别相关(不多是变量和常数章节,它只是将您返回对象章节(。
我还发现,一个有用的工具来查看解析器的工作原理是解析器的宝石。安装后(gem install parser
(,您可以开始检查不同的代码以查看解析器对它们的作用。
该宝石还捆绑了ruby-parse
实用程序,该实用程序使您可以检查Ruby解析不同代码片段的方式。-E
和-L
选项对我们来说是最有趣的,并且如果我们只想处理诸如foo = 'bar'
之类的Ruby的片段,则必须使用-e
选项。例如:
> ruby-parse -E -e "foo = 'bar'"
foo = 'bar'
^~~ tIDENTIFIER "foo" expr_cmdarg [0 <= cond] [0 <= cmdarg]
foo = 'bar'
^ tEQL "=" expr_beg [0 <= cond] [0 <= cmdarg]
foo = 'bar'
^~~~~ tSTRING "bar" expr_end [0 <= cond] [0 <= cmdarg]
foo = 'bar'
^ false "$eof" expr_end [0 <= cond] [0 <= cmdarg]
(lvasgn :foo
(str "bar"))
ruby-parse -L -e "foo = 'bar'"
s(:lvasgn, :foo,
s(:str, "bar"))
foo = 'bar'
~~~ name
~ operator
~~~~~~~~~~~ expression
s(:str, "bar")
foo = 'bar'
~ end
~ begin
~~~~~ expression
在顶部链接的两个参考都突出显示边缘情况。红宝石文档使用了示例p a if a = 0.zero?
whlie ruby hacking Guide使用了等效的示例p(lvar) if lvar = true
,两者都提出了NameError
。
sidenote:记住=
的意思是分配,==
表示比较。边缘情况下的if foo = true
构造告诉Ruby检查表达式foo = true
是否评估为true。换句话说,它将值true
分配给foo
,然后检查该分配的结果是否为true
(将是(。这很容易与更常见的if foo == true
混淆,CC_27简单地检查foo
是否与true
进行了比较。因为两者是如此容易混淆,所以Ruby会在条件:warning: found `= literal' in conditional, should be ==
中使用分配运算符时发出警告。
使用ruby-parse
实用程序,让我们将原始示例foo = 'bar' if false
与该边缘情况进行比较,foo if foo = true
:
> ruby-parse -L -e "foo = 'bar' if false"
s(:if,
s(:false),
s(:lvasgn, :foo,
s(:str, "bar")), nil)
foo = 'bar' if false
~~ keyword
~~~~~~~~~~~~~~~~~~~~ expression
s(:false)
foo = 'bar' if false
~~~~~ expression
s(:lvasgn, :foo,
s(:str, "bar"))
foo = 'bar' if false # Line 13
~~~ name # <-- `foo` is a name
~ operator
~~~~~~~~~~~ expression
s(:str, "bar")
foo = 'bar' if false
~ end
~ begin
~~~~~ expression
您可以在输出的第13和14行上看到,在原始示例中,foo是一个名称(即变量(。
> ruby-parse -L -e "foo if foo = true"
s(:if,
s(:lvasgn, :foo,
s(:true)),
s(:send, nil, :foo), nil)
foo if foo = true
~~ keyword
~~~~~~~~~~~~~~~~~ expression
s(:lvasgn, :foo,
s(:true))
foo if foo = true # Line 10
~~~ name # <-- `foo` is a name
~ operator
~~~~~~~~~~ expression
s(:true)
foo if foo = true
~~~~ expression
s(:send, nil, :foo)
foo if foo = true # Line 18
~~~ selector # <-- `foo` is a selector
~~~ expression
在边缘案例示例中,第二个foo也是一个变量(第10和11行(,但是当我们查看第18行和19行时,我们看到第一个foo已被识别为选择器(即,是一种方法(。
这表明解析器决定事物是方法还是变量,并且它以不同的顺序解析与以后的评估方式不同。
考虑边缘情况...
解析器运行时:
- 它首先将整行视为单个表达式
- 然后将其分解为两个由
if
关键字隔开的表达式 - 第一个表达式
foo
以较低的案例字母开头,因此必须是一种方法或变量。它不是现有的变量,也不遵循分配运算符,因此解析器得出结论,它必须是一种方法 - 第二个表达式
foo = true
被分解为表达式,操作员,表达式。同样,表达式foo
也从较低的案例字母开始,因此必须是方法或变量。它不是现有变量,但随后是分配运算符,因此解析器知道将其添加到本地变量列表中。
稍后评估器运行:
- 它将首先将
true
分配给foo
- 然后将执行条件并检查该分配的结果是否为(在这种情况下为(
- 然后将调用
foo
方法(除非我们使用method_missing
处理(。