【问题标题】:metaprograming String#scan and globals?元编程 String#scan 和全局变量?
【发布时间】:2013-11-05 21:21:25
【问题描述】:

我的目标是将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 方法版本中设置,但由于它是本地的,它不会返回到使用该变量的原始调用者。

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

如果它有帮助的话,到目前为止我看到的所有$& 变量的使用都在传递给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”。如果我们能做到这一点,我会很高兴的!

【问题讨论】:

  • 你遇到了全局变量的问题;它们可以在任何地方改变,并影响需要看到它们的一切。
  • 这实际上与正在发生的事情相反——“全局”变量不在它们需要的绑定中。(它们不是真正的全局变量,这正是文档所说的. 如果有人能解释为什么我想听)。
  • 啊,我明白你在说什么。有趣的。比较从 1.8.7 到 1.9.3 到 2.0 的行为并查看沿线某处是否有变化可能会很有趣。
  • 在您的扫描方法版本中破解对象实例评估是否合适?像Object.$& = $& 这样的东西。在它之前你应该定义attr_accessor :$& 和可能的覆盖阅读器。
  • 哎呀,祝你好运。如果那些“特殊变量”是真正的全局变量,会简单得多。我对此并不擅长,但似乎MRI ruby's parser 对待这些变量的方式与其他全局变量不同;也许如果你能找到解析器如何评估特殊变量,你就会知道如何首先访问/写入它们。但是我敢打赌,您将不得不为此弄乱 C 或解析器...

标签: ruby-on-rails ruby metaprogramming


【解决方案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​​ 的来源。

【讨论】:

    【解决方案2】:

    您不能设置$&,因为它派生自$~,即最后一个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
    

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

    【讨论】:

    • 看起来很有希望!让我试试看。
    • @bchurchill 进展如何?
    • 看起来有效!事实证明,在 Ruby 2.0 中有一种方法可以通过 DebugInspector 获取调用者的绑定;看看stackoverflow.com/questions/1356749/…。我可以设置 $~ 的事实让一切都顺利!
    猜你喜欢
    • 2012-04-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-14
    • 1970-01-01
    • 2022-01-28
    • 2015-07-07
    相关资源
    最近更新 更多