什么时候解析一行Ruby?评价?执行?

When is a line of Ruby parsed? evaluated? executed?

我有点惊讶地发现 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 内部结构并找出在这一特定场景中发生的事情?

回答我自己的问题,Jay's linked to a section of the docs 解释的地方:

The local variable is created when the parser encounters the assignment, not when the assignment occurs

Ruby Hacking Guide 中对此有更深入的分析(没有可用的部分链接,搜索或滚动到 "Local Variable Definitions" 部分):

By the way, it is defined when “it appears”, this means it is defined even though it was not assigned. The initial value of a defined [but not yet assigned] variable is nil.

这回答了最初的问题,但没有回答如何了解更多。


Jay 和 Pat Shaughnessy 的 simonwo both suggested Ruby Under a Microscope 我很想读。

此外,Ruby 黑客指南的其余部分涵盖了很多细节,并实际检查了底层 C 代码。 Objects and Parser chapters were particularly relevant to the original question about variable assignment (not so much the Variables and constants 一章,它只是让你回到对象一章。

我还发现 Parser gem 是查看解析器工作原理的有用工具。安装后 (gem install parser) 您可以开始检查不同的代码位以查看解析器对它们做了什么。

gem 还捆绑了 ruby-parse 实用程序,让您可以检查 Ruby 解析不同代码片段的方式。我们最感兴趣的是 -E-L 选项,如果我们只想处理 Ruby 的片段,例如 foo = 'bar',则 -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

顶部链接的两个参考都突出显示了边缘情况。 Ruby 文档 使用了示例 p a if a = 0.zero?Ruby Hacking Guide 使用了等效示例 p(lvar) if lvar = true,两者都引发了 NameError.

旁注:记住=表示赋值,==表示比较。边界情况下的 if foo = true 构造告诉 Ruby 检查表达式 foo = true 是否为真。换句话说,它将值 true 分配给 foo,然后检查该分配的结果是否为 true(它将是)。这很容易与更常见的 if foo == true 混淆,后者只是检查 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 方法(它将引发 NameError,除非我们使用 method_missing 处理它)。