以下是根据问题提供的一种方法(适用于Ruby 2)。虽然不太美观,而且在某些方面并不完美,但可以完成工作。
def newsub(str, *rest, &bloc)
str =~ rest[0]
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
1.times do |i|
p s
p $1
end
$&
、
$1
、
$2
等特殊变量(相关的还有
$~
(
Regexp.last_match
)、
$'
)只在局部范围内有效。在Ruby中,局部范围继承了父级作用域中同名的变量。
s
变量和
$1
变量在上面的例子中都被
继承了。方法
1.times
使用
do
块
yield,但是该方法无法控制块内的变量,除了块参数(例如上面例子中的
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#match
、Regexp#=~
、Regexp#===
、String#=~
、String#sub
、String#gsub
、String#scan
、Enumerable#all?
和Enumerable#grep
。
提示1:String#split
似乎总是将$~
重置为nil。
提示2:Regexp#match?
和String#match?
不更新$~
,因此速度更快。
这里有一小段代码片段来说明作用域如何工作:
def sample(str, *rest, &bloc)
str.sub(*rest, &bloc)
$1
end
sample('abc', /(c)/){}
p $1
在这里,$1
在方法sample()
中由同一作用域中的str.sub
设置。 这意味着方法sample()
将无法(简单地)引用给定块中的$1
。
我在Ruby官方文档(Ver.2.5.1)的正则表达式部分中指出以下声明:
使用字符串和正则表达式的=~
运算符成功匹配后会设置$~
全局变量。
这是相当具有误导性的,因为
$~
是一个预定义的局部作用域变量(不是全局变量),而且
$~
被设置(可能为空)无论上次尝试的匹配是否成功。
像$~
和$1
这样的变量不是全局变量可能会稍微令人困惑。但是,它们是有用的符号,不是吗?