8

我的目标是用其他可以完成额外工作的方法替换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 代码调用scanwhich 使用了该$&变量。该变量在我的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”。使用修改后的字符串类,它会打印两个空行(因为$&was nil),然后返回“hello world”。如果我们能做到这一点,我会很高兴的!

4

2 回答 2

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

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

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

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

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

于 2013-11-01T15:15:15.293 回答
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 $&包含一个有效值,但在块内部它变成无效值。我猜这是由于更改过程/线程上下文期间的奇点堆栈和变量恢复,因为:call函数可能无法访问:scan本地状态。

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

于 2013-11-01T07:28:31.773 回答