metaprograming String#scan和globals?

bch*_*ill 8 ruby metaprogramming ruby-on-rails

我的目标是String用其他方法替换类中的方法(这是针对研究项目).通过在String类中编写代码,这适用于许多方法

alias_method :center_OLD, :center
def center(args*)
  r = self.send(*([:center_OLD] + args))
  #do some work here 
  #return something
end
Run Code Online (Sandbox Code Playgroud)

对于某些方法,我也需要处理Proc,这没问题.但是,对于该scan方法,调用它具有从正则表达式匹配设置特殊全局变量的副作用.如文档所述,这些变量是线程和方法的本地变量.

不幸的是,一些Rails代码调用scan使用$&变量.该变量在我的scan方法版本中设置,但因为它是本地的,它不会使它回到使用该变量的原始调用者.

有没有人知道解决这个问题的方法?如果问题需要澄清,请告诉我.

如果它$&有所帮助,我到目前为止看到的变量的所有用途都在Proc传递给scan函数内部,所以我可以获得该Proc的绑定.但是,用户似乎根本无法改变$&,因此我不知道这将有多大帮助.

现行守则

class String
  alias_method :scan_OLD, :scan
  def scan(*args, &b)
    begin

      sargs = [:scan_OLD] + args

      if b.class == Proc
        r = self.send(*sargs, &b)
      else
        r = self.send(*sargs)
      end
      r

    rescue => error
      puts error.backtrace.join("\n")
    end
  end
end
Run Code Online (Sandbox Code Playgroud)

当然,我会在返回之前做更多的事情r,但这甚至是有问题的 - 所以为了简单起见,我们会坚持这一点.作为测试用例,请考虑:

"hello world".scan(/l./) { |x| puts x }
Run Code Online (Sandbox Code Playgroud)

无论有没有我的版本,这都可以正常工作scan.使用"vanilla" String类可以产生同样的效果

"hello world".scan(/l./) { puts $&; }
Run Code Online (Sandbox Code Playgroud)

即,它打印"ll"和"ld"并返回"hello world".随着修改后的字符串类它打印两个空行(因为$&nil),然后返回的"hello world".如果我们能够做到这一点,我会很高兴的!

ere*_*gon 4

您无法设置$&,因为它源自$~最后一个 MatchData。但是,$~可以设置并且实际上可以满足您的要求。诀窍是将其设置在块绑定中。

该代码的灵感来自Pathname 的旧 Ruby 实现
(新代码是C语言,不需要关心Ruby框架局部变量)

class String
  alias_method :scan_OLD, :scan
  def scan(*args, &block)
    sargs = [:scan_OLD] + args

    if block
      self.send(*sargs) do |*bargs|
        Thread.current[:string_scan_matchdata] = $~
        eval("$~ = Thread.current[:string_scan_matchdata]", block.binding)
        yield(*bargs)
      end
    else
      self.send(*sargs)
    end
  end
end
Run Code Online (Sandbox Code Playgroud)

线程本地(实际上是光纤本地)变量的保存似乎没有必要,因为它仅用于传递值,并且线程永远不会读取除最后一组值之外的任何其他值。它可能是为了恢复原始值(很可能是nil因为该变量不存在)。

完全避免线程局部变量的一种方法是创建一个$~lambda 的 setter(但它确实为每个调用创建一个 lambda):

self.send(*sargs) do |*bargs|
  eval("lambda { |m| $~ = m }", block.binding).call($~)
  yield(*bargs)
end
Run Code Online (Sandbox Code Playgroud)

对于其中任何一个,您的示例都有效!