如何在Ruby中将Regexp.last_match传递给一个块

5
有没有办法在Ruby中将最后匹配(实际上是Regexp.last_match)传递给块(迭代器)?下面是一个示例方法,作为String#sub的一种包装器来演示问题。它接受标准参数和块:
def newsub(str, *rest, &bloc)
  str.sub(*rest, &bloc)
end

它适用于标准参数情况,并且可以接受一个块;但是像$1、$2等位置特殊变量在块内无法使用。以下是一些示例:

newsub("abcd", /ab(c)/, '\1')        # => "cd"
newsub("abcd", /ab(c)/){|m| $1}      # => "d"  ($1 == nil)
newsub("abcd", /ab(c)/){$1.upcase}   # => NoMethodError

这个块不像String#sub(/..(.)/){$1}一样工作的原因,我猜与作用域有关。特殊变量$1、$2等是局部变量,所以Regexp.last_match也是局部变量。

有没有什么方法可以解决这个问题?我想让newsub方法像String#sub一样工作,这样$1、$2等就可以在提供的块中使用。
根据过去的回答,可能没有办法实现这一点...

1
这个问题,我认为值得点赞。 - Cary Swoveland
@CarySwoveland 谢谢。我现在终于完全理解了背景情况 - 我已经发布了一个相当全面(尽管很长)的答案来总结它。 - Masa Sakano
@CarySwoveland,事实上我已经找到了一种方法!我在我的答案中进行了重大更新以解释它。 - Masa Sakano
1个回答

4

以下是根据问题提供的一种方法(适用于Ruby 2)。虽然不太美观,而且在某些方面并不完美,但可以完成工作。

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

通过这样做,结果如下:
_ = (/(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">

深入分析

在上述定义的方法newsub中,当给定一个块时,在块执行后,调用方线程中的局部变量$1等被(重新)设置,这与String#sub一致。然而,当没有给定块时,调用方线程中的局部变量$1等不会被重置,而在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
$&$1$2等特殊变量(相关的还有$~Regexp.last_match)、$')只在局部范围内有效。在Ruby中,局部范围继承了父级作用域中同名的变量。s变量和$1变量在上面的例子中都被继承了。方法1.times使用doyield,但是该方法无法控制块内的变量,除了块参数(例如上面例子中的i;注意:Integer#times不提供任何块参数,尝试在块中接收一个或多个参数也会被默默忽略)。
这意味着,yield块的方法无法控制块中的$1$2等本地变量(尽管它们看起来像全局变量)。

String#sub方法的情况

现在,让我们分析一下带有块的String#sub方法是如何工作的:
'abc'.sub(/.(.)./){ |m| $1 }

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

因此,如果方法sub不同的作用域中定义了该块,则sub方法对块内的局部变量(包括$1)没有任何控制。不同的作用域意味着sub方法是用Ruby代码编写和定义的情况,或者实际上,除了一些不是用Ruby而是用于编写Ruby解释器的相同语言编写的方法之外的所有Ruby方法。

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

在这里,$1 在方法sample()由同一作用域中的str.sub设置。 这意味着方法sample()将无法(简单地)引用给定块中的$1

我在Ruby官方文档(Ver.2.5.1)的正则表达式部分中指出以下声明:

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

这是相当具有误导性的,因为
  1. $~是一个预定义的局部作用域变量(不是全局变量),而且
  2. $~被设置(可能为空)无论上次尝试的匹配是否成功。
$~$1这样的变量不是全局变量可能会稍微令人困惑。但是,它们是有用的符号,不是吗?

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