元编程字符串#扫描和全局



我的目标是用其他做额外工作的方法替换String类中的方法(这是针对一个研究项目)。 这适用于许多方法,方法是在 String 类中编写类似于

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

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

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

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

如果它有帮助,到目前为止我看到的 $& 变量的所有用途都在传递给 scan 函数的 Proc 中,因此我可以获取该 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

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

"hello world".scan(/l./) { |x| puts x }

无论有没有我的scan版本,这都可以正常工作。 对于"香草"String类,这会产生与

"hello world".scan(/l./) { puts $&; }

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

你不能设置$&,因为它是从最后一个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

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

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

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

使用其中任何一个,您的示例都有效!

我编写了简单的代码来模拟这个问题:

"hello world".scan(/l./) { |x| puts x }
"hello world".scan(/l./) { puts $&; }
class String
   alias_method :origin_scan, :scan
   def scan *args, &b
      args.unshift :origin_scan
      @mutex ||= Mutex.new
      begin
         self.send *args do |a|
            break if !block_given?
            @mutex.synchronize do
               p $& 
               case b.arity
               when 0
                  b.call
               when 1
                  b.call a
               end
            end
         end
      rescue => error
         p error, error.backtrace.join("n")
      end
   end
end
"hello world".scan(/l./) { |x| puts x }
"hello world".scan(/l./) { puts $& }

并找到了以下内容。变量的包含更改$&:call函数内部发生,即在包含有效值之前的第 3 步:call $&,但在块内部它变为无效值。我想这是由于更改过程/线程上下文期间的奇点堆栈和变量恢复,因为:call函数可能无法访问:scan本地状态。

我看到两种变体:第一种是避免在特定函数重定义中使用全局变量,第二种是可以更深入地挖掘 ruby 的来源。

最新更新