基于 Ruby 语法的未定义本地变量

7
在以下的Ruby代码中,
#! /usr/bin/env ruby
x = true
y = x and z = y
puts "z: #{z}"

这段代码会输出z: true,与预期相符。

但是在下面这个例子中,我原以为行为相同:

#! /usr/bin/env ruby
x = true
z = y if y = x
puts "z: #{z}"

这会导致

未定义本地变量或主对象的方法“y”(NameError)

为什么会这样呢?

我理解我正在进行一项任务,并隐式检查分配值以确定是否运行z = y。我也明白,如果我在x = 5行之后添加了y的声明y = nil,它将通过并按预期运行。

但是,难道不应该期望语言首先评估if部分,然后再评估其内容,第二个代码块的行为与第一个代码块相同吗?


1
这是一个非常好的问题,因为 z = 2 if y = x 可以正常工作,而 z = w if false 也可以。 - Ry-
4
这是 Ruby 中的一个棘手问题。有一些不太明确的条款,大致意思是“如果解析器遇到赋值语句”(这意味着不必执行赋值语句就能创建局部绑定)……但在像这样的情况和其他几种情况下,它会出现故障。参考:http://stackoverflow.com/questions/25783428/why-is-a-variable-declared-when-it-appears-in-if-false - user2864740
1
@MarcBaumbach 这不是“错误”。这是“预期行为”,即使实际上并不是很..预期。 - user2864740
2
我猜测那些负责使裸露的 x=x 不会出现 NameError 的小精灵也在这个问题中留下了他们的爪痕。 - roippi
2
@roippi x=x 不是错误,因为赋值 x= 在调用 x 之前。 - sawa
显示剩余7条评论
1个回答

4

简述

这实际上与解释器有关。问题出现在MRI Ruby 2.1.2和JRuby 1.7.13中,但在Rubinius中按预期工作。例如,在Rubinius 2.2.10中:

x = true
z = y if y = x
#=> true

在MRI中,使用Ripper进行一些探索后发现,即使AST赋值相似,Ruby在处理后置条件时也会有所不同。实际上,当构建AST时,它会使用不同的标记来表示后置条件,这似乎对赋值表达式的评估顺序产生了影响。这是否应该是这种情况,或者是否可以修复,这是一个需要Ruby核心团队解答的问题。

为什么逻辑与能够正常工作

x = true
y = x and z = y

这是成功的,因为它实际上是两个连续的赋值操作,因为x被赋值为true并且被解析为真值。由于第一个表达式是真值,连接逻辑and的下一个表达式也被求值并且同样被解析为真值。
y = x
#=> true

z = y
#=> true

换句话说,x 被赋值为 true,然后 z 也被赋值为 true。在任何时候,两个赋值的右侧都不是未定义的。

为什么它在后置条件下失败

x = true
z = y if y = x

在这种情况下,后置条件实际上是首先被评估的。您可以通过查看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]]]]]]]

与您的第一个示例不同,在第一个表达式中,y 被赋值为 true,因此在被分配给 z 之前,在第二个表达式中解析为 true。而在这种情况下,在 y 仍未定义时进行评估。这会引发一个 NameError
当然,有人可以合理地认为两个表达式都包含赋值,并且如果 Ruby 的解析器像普通 if 语句一样首先评估 y = x,那么 y 实际上就不是未定义的(请参见下面的 AST)。这可能只是后置条件 if 语句和 Ruby 处理 :if_mod 标记的方式的怪癖。

使用 :if 而不是 :if_mod 标记成功

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

x = true
if y = x
  z = y
end
#=> true

观察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]]]

请注意,唯一的真正区别是引发 NameError 的示例使用了 :if_mod,而成功的版本使用了 :if。显然,后置条件是导致您看到的错误、怪癖或缺陷的原因。
如何处理它
这种解析行为可能有很好的技术原因,也可能没有。我没有资格判断。但是,如果您认为这是一个错误,并且有动力采取措施,最好的方法是检查 Ruby Issue Tracker 是否已经报告了这个问题。如果没有,也许是时候有人正式提出了。

Rubinius在其后置条件AST中使用:if而不是:if_mod:require 'pp'; require 'parser/current'; pp Parser::CurrentRuby.parse 'z = y if y = x' - Todd A. Jacobs
很好的细节..而且很好地避免了争论。我想现在我终于有一个链接问题,以便下次再次出现时使用。 - user2864740
这看起来像是Rubinius中的一个bug。 - Jörg W Mittag
@JörgWMittag 为什么?由于内联 if 的条件首先被处理,因此解析器应该以类似的方式处理它是有意义的。 - David Unric
与此相关的Twitter讨论解释了这是一个带有:if_mod标记的LALR下降问题。 - Todd A. Jacobs

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接