正则表达式// 不匹配任何字符串;它匹配任何 零长度 字符串。
但是让我们备份。考虑一下:
"ab".scan(/[a-z]/) # => ["a", "b"]
这很容易理解。 String#scan 从索引 0 开始并立即找到匹配项 a,并将其添加到其结果数组中,现在为 ["a"]。然后它将其内部“光标”推进到匹配文本之后的第一个索引,恰好是 1,它再次找到匹配项。现在它的结果是["a", "b"]。最后,它前进到索引 2,它超过了字符串的末尾,所以它停止了。
现在考虑你的例子:
"abc".scan(//)
这几乎是一样的。 String#scan 从索引 0 开始并立即找到匹配项:a 之前的零长度字符串。它将它添加到它的结果数组中,现在是[""]。但现在我们遇到了麻烦。因为匹配的长度是0,所以String#scan在匹配后前进到第一个索引时,仍然是索引0,应该是无限循环吧?
此时,正则表达式引擎做出执行决定。因为前一个匹配的长度为零,所以它不会陷入无限循环,而是将光标前移一个字符。现在索引为 1,然后它找到另一个匹配项:b 之前的零长度字符串。现在它的结果是["", ""]。它再次将光标前移一个字符并在c 之前找到长度为零的字符串,然后在字符串结尾之前(c 之后)再次找到长度为零的字符串。那么它的最终结果就是["", "", "", ""]。
这是一个方便的调试习惯用法String#scan:
"abc".scan(//) do
p $~, $~.offset(0)
end
$~ 是Regexp.last_match 的别名,它返回最后一个匹配的MatchData 对象。 $~.offset(0) 返回一个包含匹配开始和结束索引的数组($~.offset(1) 将返回第一个捕获组的开始和结束,依此类推)。在这种情况下,我们得到:
#<MatchData "">
[0, 0]
#<MatchData "">
[1, 1]
#<MatchData "">
[2, 2]
#<MatchData "">
[3, 3]
如您所见,每次匹配一个空字符串("")时,第一个匹配在索引 0 处,最后一个匹配在索引 3 处(“在”字符串中的最后一个字符之后)。在 repl.it 上试用:https://repl.it/GL3N
您可以在(当然)Regular-Expressions.info 阅读有关不同正则表达式引擎如何处理零长度匹配的更多信息。