为什么Ruby中的正则表达式对象被认为是“虚假”的?

20
Ruby有一个普遍的“真值”和“假值”的概念。Ruby确实有两个特定的布尔对象类TrueClassFalseClass,由特殊变量truefalse分别表示其单例实例。然而,“真值”和“假值”不仅限于这两个类的实例,这个概念是“普遍的”,适用于Ruby中的每个对象。每个对象都是“真值”或“假值”。规则非常简单。特别地,“只有两个对象是假值”:
- nilNilClass的单例实例和 - falseFalseClass的单例实例
“每一个其他的对象”都是“真值”。这甚至包括在其他编程语言中被认为是“假值”的对象。

这些规则是编程语言内置的,不可由用户定义。没有类似于to_bool的隐式转换。

以下是ISO Ruby Language Specification中的一句话:

6.6 布尔值

一个对象被归类为真对象假对象

只有falsenil是假对象。 false是类FalseClass的唯一实例(见15.2.6),其中包含一个假表达式 (见11.5.4.8.3)。 nil是类NilClass的唯一实例(见15.2.4),其中包含一个nil表达式 (见11.5.4.8.2)。

除了falsenil之外的对象都被归类为真对象。 true是类TrueClass的唯一实例(见15.2.5),其中包含一个真表达式 (见11.5.4.8.3)。

可执行的Ruby/Spec似乎也同意这个定义

it "considers a non-nil and non-boolean object in expression result as true" do
  if mock('x')
    123
  else
    456
  end.should == 123
end
根据这两个来源,我会认为Regexp也是真值,但是根据我的测试,它们并不是:
if // then 'Regexps are truthy' else 'Regexps are falsy' end
#=> 'Regexps are falsy'

我在 YARV 2.7.0-preview1, TruffleRuby 19.2.0.1JRuby 9.2.8.0 上进行了测试。这三个实现彼此一致,但与 ISO Ruby 语言规范和我的 Ruby/Spec 解释不一致。
更准确地说,由于评估正则表达式字面量而产生的 Regexp 对象是 falsy,而由于其他表达式而产生的 Regexp 对象是 truthy
r = //
if r then 'Regexps are truthy' else 'Regexps are falsy' end
#=> 'Regexps are truthy'

这是一个bug还是期望的行为?


!!// 是假的,但 !!/r/ 是真的。确实很奇怪。 - max
抱歉,我的错 @3limin4t0r。你是对的。我一定做了什么非常愚蠢的事情,比如漏掉了一个感叹号。 - max
2
一个假设,我认为 if // then 中的 // 被解释为一个测试(if //=~nil then 的快捷方式)(无论模式如何始终为假),而不是作为一个正则表达式实例。 - Casimir et Hippolyte
我认为 if // 将会像 cli 用法一样作用于 $_.. 例如:seq 10 15 | ruby -ne 'print if /[35]/'seq 10 15 | ruby -ne 'print if $_ =~ /[35]/' 是相同的。 - Sundeep
如上所述,!!/r/ #=> false,但前缀的 ! 实际上是 BasicObject#! 的语法糖,因此写成 /r/.!.!,无论 $_ 设置为什么,都将返回 true,这与 Ruby 的预期相符。 - Travis
显示剩余3条评论
2个回答

10

这不是一个bug。发生的事情是Ruby正在重写这段代码,以便

if /foo/
  whatever
end

有效地成为

if /foo/ =~ $_
  whatever
end

如果您正在正常脚本中运行此代码(而不是使用-e选项),则应该会看到警告:

warning: regex literal in condition

这可能会让大多数人感到有些困惑,这也是为什么会给出警告的原因,但对于使用-e选项的单行命令很有用。例如,您可以从文件中打印与给定正则表达式匹配的所有行:

这通常很令人困惑,所以我们发出了警告,但对于使用-e选项的单行命令非常有用。例如,您可以通过以下方式打印来自文件中与给定正则表达式匹配的所有行:

$ ruby -ne 'print if /foo/' filename

(print函数的默认参数也是 $_。)


请参阅-n-p-a-l选项,以及仅在使用-n-p时才可用的少数内核方法(chompchopgsubsub)。 - matt
还有解析器的第二部分,在那里发出了警告。不过我不知道那里正在发生什么。 - matt
我认为“第二部分”才是实际适用于这个问题的部分。NODE_LIT 的类型是 T_REGEXP。你在答案中发布的是针对动态Regexp字面量的,即使用插值的Regexp字面量,例如/#{''}/ - Jörg W Mittag
@JörgWMittag 我认为你是对的。在编译器和生成的字节码中查找,看起来在动态正则表达式的情况下,解析树被重写以显式添加 $_ 作为节点,编译器将其处理为正常情况,而在静态情况下则由编译器处理所有内容。这对我来说很遗憾,因为“嘿,你可以看到解析树在这里被重写”的回答很好。 - matt

6

据我所知,这是 Ruby 语言中一项未经记录的功能的结果,最好通过 this spec 进行解释:

it "matches against $_ (last input) in a conditional if no explicit matchee provided" do
  -> {
    eval <<-EOR
    $_ = nil
    (true if /foo/).should_not == true
    $_ = "foo"
    (true if /foo/).should == true
    EOR
  }.should complain(/regex literal in condition/)
end

你可以将$_通常视为“由gets读取的最后一个字符串”
更让人困惑的是,$_(以及$-)不是全局变量;它具有局部作用域
当一个Ruby脚本开始时,$_ == nil
因此,代码如下:
// ? 'Regexps are truthy' : 'Regexps are falsey'

被解释为:

(// =~ nil) ? 'Regexps are truthy' : 'Regexps are falsey'

...这将返回falsey。

另一方面,对于非字面正则表达式(例如r = //Regexp.new('')),这种特殊解释不适用。

//是truthy的;与除nilfalse之外的所有其他ruby对象一样。


除了在命令行直接运行ruby脚本(即使用-e标志),否则ruby解释器会对此类用法显示警告:

warning: regex literal in condition

您可以在脚本中利用这种行为,例如:
puts "Do you want to play again?"
gets
# (user enters e.g. 'Yes' or 'No')
/y/i ? play_again : back_to_menu

...但更正常的做法是将一个本地变量分配给gets的结果,并显式地对该值执行正则表达式检查。

我不知道有任何使用情况需要使用空正则表达式进行此检查,特别是当其定义为字面值时。你所强调的结果确实会让大多数Ruby开发人员措手不及。


如果在运行上述代码之前手动设置 $_='hello world',则应该得到不同的结果——因为 // =~ 'hello world',但不匹配 nil - Tom Lord
不,我的意思是!//在没有条件的情况下评估为true。你引用的规范是关于条件语句中的正则表达式字面量的,但在这个例子中,没有条件语句,所以这个规范不适用。 - Jörg W Mittag
@JörgWMittag 注意 //.! 评估为 false,所以正则表达式本身确实是 true。 - mrzasa
2
啊..是的,非常令人惊讶。不过这种行为似乎是有联系的:puts !//; $_ = ''; puts !// -- 我猜测是因为解析器像宏一样展开了它;它不一定需要在条件语句内部? - Tom Lord
我要向所有声称 Ruby 很容易或简单的人展示这个。我真的很喜欢 Ruby,但它绝对不简单。 - Jörg W Mittag
显示剩余2条评论

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