基于Ruby语法的未定义局部变量

Jim*_*Chu 7 ruby

在以下Ruby代码中,

#! /usr/bin/env ruby
x = true
y = x and z = y
puts "z: #{z}"
Run Code Online (Sandbox Code Playgroud)

它将z: true按预期输出.

但在下面的一个中,我希望它具有相同的行为:

#! /usr/bin/env ruby
x = true
z = y if y = x
puts "z: #{z}"
Run Code Online (Sandbox Code Playgroud)

它导致了

未定义的局部变量或方法'y'表示main:Object(NameError)

这是为什么?

我理解我正在做一个赋值,并隐式检查赋值以确定是否运行z = y.我也明白,如果我添加y的声明y = nil,就x = 5在行之后,它将按预期传递并运行.

但是,期望语言if首先评估部分然后评估其内容,并且第二块代码与第一块代码的行为相同,这是不正确的?

Tod*_*obs 4

长话短说

这实际上是特定于解释器的。该问题出现在 MRI Ruby 2.1.2 和 JRuby 1.7.13 中,但在Rubinius中按预期工作。例如,对于 Rubinius 2.2.10:

x = true
z = y if y = x
#=> true
Run Code Online (Sandbox Code Playgroud)

在 MRI 中,使用Ripper进行的一些探索表明,即使AST分配相似,Ruby 也会以不同的方式处理后置条件。它实际上在构建 AST 时对后置条件使用不同的标记,这似乎对赋值表达式的求值顺序有影响。是否应该出现这种情况,或者是否可以修复,这是Ruby 核心团队的问题。

为什么它适用于逻辑与

x = true
y = x and z = y
Run Code Online (Sandbox Code Playgroud)

这是成功的,因为它实际上是按顺序进行的两个赋值,因为true被分配给x,因此计算结果为真。由于第一个表达式为真,因此由逻辑and连接的下一个表达式也被求值,并且同样被求值为真。

y = x
#=> true

z = y
#=> true
Run Code Online (Sandbox Code Playgroud)

换句话说,x被赋予值true,然后z也被赋予值true。任一赋值的右侧都不会未定义。

为什么它会因后置条件而失败

x = true
z = y if y = x
Run Code Online (Sandbox Code Playgroud)

在这种情况下,实际上首先评估后置条件。您可以通过查看AST看到这一点:

require 'pp'
require 'ripper'

x = true

pp Ripper.sexp 'z = y if y = x'
[:program,
 [[:if_mod,
   [:assign,
    [:var_field, [:@ident, "y", [1, 9]]],
    [:vcall, [:@ident, "x", [1, 13]]]],
   [:assign,
    [:var_field, [:@ident, "z", [1, 0]]],
    [:vcall, [:@ident, "y", [1, 4]]]]]]]
Run Code Online (Sandbox Code Playgroud)

与您的第一个示例不同,其中y在第一个表达式中分配true,因此true在分配给z之前在第二个表达式中解析为,在这种情况下y在仍未定义时进行评估。这会引发NameError

当然,人们可以合理地认为这两个表达式都包含赋值,并且如果 Ruby 的解析器像使用普通 if 语句一样首先进行计算,那么y就不会真正是未定义的(请参见下面的 AST)。y = x这可能只是后置条件 if 语句的一个怪癖以及 Ruby 处理 :if_mod 标记的方式。

使用 :if 而不是 :if_mod 令牌成功

如果你反转逻辑并使用普通的 if 语句,它就可以正常工作:

x = true
if y = x
  z = y
end
#=> true
Run Code Online (Sandbox Code Playgroud)

查看 Ripper 会产生以下 AST:

require 'pp'
require 'ripper'

x = true

pp Ripper.sexp 'if y = x; z = y; end'
[:program,
 [[:if,
   [:assign,
    [:var_field, [:@ident, "y", [1, 3]]],
    [:vcall, [:@ident, "x", [1, 7]]]],
   [[:assign,
     [:var_field, [:@ident, "z", [1, 10]]],
     [:var_ref, [:@ident, "y", [1, 14]]]]],
   nil]]]
Run Code Online (Sandbox Code Playgroud)

请注意,唯一真正的区别是引发 NameError 的示例使用 :if_mod,而成功的版本使用 :if。看来后置条件确实是您所看到的错误、怪癖或故障的原因。

该怎么办

这种解析行为可能有很好的技术原因,也可能没有。我没有资格评判。但是,如果您认为它是一个错误,并且您有动力对此采取措施,那么最好的办法是检查Ruby 问题跟踪器,看看它是否已被报告。如果没有,也许是时候有人正式提出这个问题了。