这是根据问题的一种方法(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
$&、$1、$2等的特殊变量(相关、$~(Regexp.last_match)、$'等)
在本地范围内工作。在 Ruby 中,本地作用域会继承父作用域中的同名变量。
在上面的示例中,变量s 是继承的,$1 也是如此。
do 块是由1.times yield 编辑的,方法1.times 无法控制块内的变量,除了块参数(上例中的i; nb,尽管Integer#times 不提供任何块参数,但尝试在块中接收参数将被忽略)。
这意味着 yield-sa 块无法控制块中的 $1、$2 等的方法,它们是局部变量(即使它们可能看起来像全局变量) .
字符串大小写#sub
现在,让我们来分析String#sub 与块的工作原理:
'abc'.sub(/.(.)./){ |m| $1 }
这里,sub 方法首先执行正则表达式匹配,因此像$1 这样的局部变量会自动设置。然后,它们(像$1 这样的变量)在块中被继承,因为这个块与方法“sub”在同一范围内。它们不传递从sub 到块,不同于块参数m(这是一个匹配的字符串,或等效于$&)。
因此,如果方法 sub 定义在与块不同的范围中,则 sub 方法无法控制块内的局部变量,包括 $1。 不同的范围是指 sub 方法是用 Ruby 代码编写和定义的,或者实际上是所有 Ruby 方法,除了一些不是用 Ruby 编写但使用与用于编写 Ruby 解释器。
Ruby的official document (Ver.2.5.1)在String#sub部分解释:
在块形式中,当前匹配字符串作为参数传入,$1、$2、$`、$&、$'等变量会被适当设置。
正确。在实践中,可以并且确实设置Regexp-match相关的特殊变量如$1、$2等的方法仅限于一些内置方法,包括Regexp#match、Regexp#=~、Regexp#===、String#=~ 、String#sub、String#gsub、String#scan、Enumerable#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 这样的变量不是全局变量这一事实可能有点令人困惑。但是,嘿,它们是有用的符号,不是吗?