元编程字符串#scan和全局?

时间:2013-10-26 21:38:22

标签: ruby-on-rails ruby metaprogramming

我的目标是将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方法,调用它会产生从正则表达式匹配中设置special global variables的副作用。如文档所述,这些变量是线程和方法的本地变量。

不幸的是,一些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

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

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

使用和不使用我的scan版本都可以正常使用。使用“vanilla”String类,它产生与

相同的东西
"hello world".scan(/l./) { puts $&; }

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

2 个答案:

答案 0 :(得分:4)

您无法设置$&,因为它来自$~,即最后一个MatchData。 但是,$~可以设置,实际上可以做你想要的。 诀窍是在块绑定中设置它。

代码的灵感来自the old Ruby implementation of Pathname (新代码在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

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

答案 1 :(得分:1)

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

"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函数内部,即在:call $&包含有效值之前的第3步,但在块内部变为无效。我想这是由于变换过程/线程上下文中的奇点堆栈和变量恢复,因为,:call函数可能无法访问:scan本地状态。

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