如何将 Regexp.last_match 传递给 Ruby 中的块

Mas*_*ano 5 ruby regex iterator scope yield

有没有办法将最后一个匹配(实际上Regexp.last_match)传递给 Ruby 中的块(迭代器)?

\n\n

Srring#sub这是一个示例方法,作为演示该问题的一种包装器。它接受标准参数和块:

\n\n
def newsub(str, *rest, &bloc)\n  str.sub(*rest, &bloc)\nend\n
Run Code Online (Sandbox Code Playgroud)\n\n

它适用于标准的仅参数情况,并且可以占用一个块;然而,像 $1、$2 等位置特殊变量在块内不可用。这里有些例子:

\n\n
newsub("abcd", /ab(c)/, \'\\1\')        # => "cd"\nnewsub("abcd", /ab(c)/){|m| $1}      # => "d"  ($1 == nil)\nnewsub("abcd", /ab(c)/){$1.upcase}   # => NoMethodError\n
Run Code Online (Sandbox Code Playgroud)\n\n

该块的工作方式与String#sub(/..(.)/){$1}我认为的工作方式不同的原因与范围有关;特殊变量 $1、$2 等是局部变量(也是Regexp.last_match)。

\n\n

有什么办法可以解决这个问题吗?我想让该方法像 $1、$2 等在提供的块中可用的意义上newsub一样工作。String#sub

\n\n

编辑:根据过去的一些答案,可能没有办法实现这个\xe2\x80\xa6

\n

Mas*_*ano 4

这是根据问题(Ruby 2)的一种方法。它并不漂亮,各方面也不是 100% 完美,但可以完成工作。

def newsub(str, *rest, &bloc)
  str =~ rest[0]  # => ArgumentError if rest[0].nil?
  bloc.binding.tap do |b|
    b.local_variable_set(:_, $~)
    b.eval("$~=_")
  end if bloc
  str.sub(*rest, &bloc)
end
Run Code Online (Sandbox Code Playgroud)

这样,结果如下:

_ = (/(xyz)/ =~ 'xyz')
p $1  # => "xyz"
p _   # => 0

p newsub("abcd", /ab(c)/, '\1')        # => "cd"
p $1  # => "xyz"
p _   # => 0

p newsub("abcd", /ab(c)/){|m| $1}      # => "cd"
p $1  # => "c"
p _                 # => #<MatchData "abc" 1:"c">

v, _ = $1, newsub("efg", /ef(g)/){$1.upcase}
p [v, _]  # => ["c", "G"]
p $1  # => "g"
p Regexp.last_match # => #<MatchData "efg" 1:"g">
Run Code Online (Sandbox Code Playgroud)

深入分析

在上面定义的方法中newsub,当给出一个块时,调用者线程中的局部变量 $1 等在块执行后被(重新)设置,这与 一致String#sub。但是,当未给出块时,局部变量 $1 等不会重置,而 in String#sub、 $1 等始终会重置,无论是否给出块。

此外,在此算法中,调用者的局部变量_也会被重置。在 Ruby 的约定中,局部变量_被用作虚拟变量,并且不应读取或引用其值。因此,这应该不会造成任何实际问题。如果该语句local_variable_set(:$~, $~)有效,则不需要临时局部变量。然而,在 Ruby 中却并非如此(至少从版本 2.5.1 开始)。请参阅 Kazuhiro NISHIYAMA 在[ruby-list:50708]中的评论(日语)。

一般背景(Ruby 的规范)解释

下面是一个简单的例子来强调与此问题相关的 Ruby 规范:

s = "abcd"
/b(c)/ =~ s
p $1     # => "c"
1.times do |i|
  p s    # => "abcd"
  p $1   # => "c"
end
Run Code Online (Sandbox Code Playgroud)

$&$1$2等特殊变量(相关的$~( Regexp.last_match)$'等)在局部范围内工作。在 Ruby 中,本地作用域继承父作用域中同名的变量。在上面的示例中,变量s继承的,因此也是$1。该do块是由yield编辑的1.times,并且该方法1.times无法控制块内的变量,除了块参数(i在上面的示例中;nb,虽然Integer#times不提供任何块参数,但尝试接收一个或多个块参数)一个块将被默默地忽略)。

这意味着yield -sa块的方法无法控制$1$2中的局部变量(即使它们可能看起来像全局变量)。

字符串#sub 的情况

现在,让我们分析一下String#sub该块是如何工作的:

'abc'.sub(/.(.)./){ |m| $1 }
Run Code Online (Sandbox Code Playgroud)

在这里,该方法sub首先执行正则表达式匹配,因此$1自动设置局部变量。然后,它们(像 之类的变量$1)在块中继承,因为该块与方法 "sub" 处于相同的作用域中。它们不会从块传递到块,与块参数(它是匹配的字符串,或相当于)sub不同。m$&

因此,如果该方法是在与块不同的作用域中sub定义的,则该方法无法控制块内的局部变量,包括. 不同的范围意味着该方法是使用 Ruby 代码编写和定义的,或者实际上是所有 Ruby 方法,除了一些不是用 Ruby 编写的方法,而是使用与编写 Ruby 解释器相同的语言编写的方法。sub$1sub

Ruby的官方文档(Ver.2.5.1)中的部分解释道String#sub

在块形式中,当前匹配字符串作为参数传入,$1、$2、$`、$& 和 $' 等变量将被适当设置。

正确的。实际上,能够并且确实设置与正则表达式匹配相关的特殊变量(例如 $1、$2 等)的方法仅限于一些内置方法,包括Regexp#matchRegexp#=~Regexp#===String#=~String#subString#gsubString#scanEnumerable#all?、 和Enumerable#grep
提示 1:String#split似乎$~总是重置 nil。
提示2:Regexp#match?并且String#match?不更新$~,因此速度要快得多。

下面是一个小代码片段,用于强调作用域的工作原理:

def sample(str, *rest, &bloc)
  str.sub(*rest, &bloc)
  $1    # non-nil if matches
end

sample('abc', /(c)/){}  # => "c"
p $1    # => nil
Run Code Online (Sandbox Code Playgroud)

这里,$1 在方法中sample()str.sub是在相同的范围内设置的。这意味着该方法sample()将无法(简单地)引用给$1定的块。

我指出Ruby官方文档(Ver.2.5.1)正则表达式一节中的说法

使用=~带有字符串和正则表达式的运算符,$~全局变量在成功匹配后设置。

相当具有误导性,因为

  1. $~预定义的局部范围变量(不是全局变量),并且
  2. $~无论最后尝试的匹配是否成功,都会被设置(可能为零)。

事实上,像$~和 一样的变量$1不是全局变量可能会有点令人困惑。但是,嘿,它们是有用的符号,不是吗?