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

在以下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’表示main:Object(NameError)

这是为什么?

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

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

TL; DR

这实际上是特定于解释器的。 问题出现在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 

这是成功的,因为它实际上是顺序的两个赋值,因为true被赋值给x ,因此评估为真实。 由于第一个表达式是真实的,下一个表达式由逻辑连接并且也被评估,同样评估为真实。

 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的解析器首先评估y = x ,并且正常if语句(参见下面的AST),那么y就不会真正被定义。 这可能只是后置条件if语句和Ruby处理:if_mod标记的方式的怪癖。

成功:if if而不是:if_mod Tokens

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

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

看着开膛手产生以下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。 当然,后期条件是您所看到的错误,怪癖或错误的原因。

怎么办呢

这种解析行为可能有很好的技术原因,或者可能没有。 我没资格判断。 但是,如果它看起来像是一个bug,并且你有动力去做一些事情,最好的办法就是检查Ruby Issue Tracker以查看它是否已被报告。 如果没有,也许是时候有人正式提出来了。