【问题标题】:How to pass Regexp.last_match to a block in Ruby如何将 Regexp.last_match 传递给 Ruby 中的块
【发布时间】:2019-02-20 21:05:26
【问题描述】:

有什么方法可以将 last match(实际上是 Regexp.last_match)传递给 Ruby 中的块(迭代器)?

这是一个示例方法,作为Srring#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 等可以在提供的块中使用。

编辑:根据some past answers,可能没有办法实现这一点……

【问题讨论】:

  • 这个问题,imo,值得点赞。
  • @CarySwoveland 谢谢。我想我现在终于完全理解了背景——我发布了一个相当全面(虽然很长)的答案来总结它。
  • @CarySwoveland 事实上我已经找到了办法!我在回答中进行了重大更新以解释它。

标签: ruby regex scope iterator yield


【解决方案1】:

这是根据问题的一种方法(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

这样,结果如下:

_ = (/(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中,当给定一个block时,在block执行后,调用者线程中的局部变量$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

$&amp;$1$2等的特殊变量(相关、$~Regexp.last_match)、$'等) 在本地范围内工作。在 Ruby 中,本地作用域会继承父作用域中的同名变量。 在上面的示例中,变量s继承的$1 也是如此。 do 块是由1.times yield 编辑的,方法1.times 无法控制块内的变量,除了块参数(上例中的inb,尽管Integer#times 不提供任何块参数,但尝试在块中接收参数将被忽略)。

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

字符串大小写#sub

现在,让我们来分析String#sub 与块的工作原理:

'abc'.sub(/.(.)./){ |m| $1 }

这里,sub 方法首先执行正则表达式匹配,因此像$1 这样的局部变量会自动设置。然后,它们(像$1 这样的变量)在块中被继承,因为这个块与方法“sub”在同一范围内。它们不传递sub 到块,不同于块参数m(这是一个匹配的字符串,或等效于$&amp;)。

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

Ruby的official document (Ver.2.5.1)String#sub部分解释:

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

正确。在实践中,可以并且确实设置Regexp-match相关的特殊变量如$1、$2等的方法仅限于一些内置方法,包括Regexp#matchRegexp#=~Regexp#===String#=~String#subString#gsubString#scanEnumerable#all?Enumerable#grep
提示 1:String#split 似乎总是重置 $~ nil。
提示 2:Regexp#match?String#match? 不更新 $~,因此速度更快。

这里有一个小代码 sn-p 来突出作用域的工作原理:

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)section of Regular expression中的声明

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

相当具有误导性,因为

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-08-07
    • 2012-07-04
    • 1970-01-01
    • 2014-01-05
    • 2014-11-26
    • 2012-03-15
    • 1970-01-01
    相关资源
    最近更新 更多