【问题标题】:Using $1, $2, etc. global variables inside method definition在方法定义中使用 $1、$2 等全局变量
【发布时间】:2013-09-04 05:13:41
【问题描述】:

给定以下两段代码:

def hello(z)
  "hello".gsub(/(o)/, &z)
end
z = proc {|m| p $1}
hello(z)
# prints: nil

def hello
  z = proc {|m| p $1}
  "hello".gsub(/(o)/, &z)
end
hello
# prints: "o"

为什么这两段代码的输出不同?有没有办法从方法定义外部将块传递给gsub,以便变量$1$2 的评估方式与在方法定义内部给出块的方式相同?

【问题讨论】:

    标签: ruby scope pseudo-globals


    【解决方案1】:

    为什么输出不同?

    ruby 中的 proc 具有词法范围。这意味着当它发现一个未定义的变量时,它会在 proc 被定义而不是调用的上下文中解析。这解释了您的代码的行为。

    您可以看到块是在正则表达式之前定义的,这可能会导致混淆。这个问题涉及一个神奇的 ruby​​ 变量,它的工作方式与其他变量完全不同。 Citing @JörgWMittag

    这很简单,真的:$SAFE 的行为不像你对全局变量的期望那样是因为它不是一个全局变量。这是一只神奇的独角兽。

    在 Ruby 中有很多神奇的独角兽 thingamajiggies,不幸的是,它们没有很好的文档记录(实际上根本没有文档记录),因为替代 Ruby 实现的开发人员发现了困难的方法。这些 thingamajiggies 的行为都不同且(似乎)不一致,而它们唯一的两个共同点就是它们看起来像全局变量,但行为却不像它们。

    有些具有本地范围。有些具有线程本地范围。有些神奇地改变了,没有任何人分配给他们。有些对解释器具有神奇的意义,并改变了语言的行为方式。有些还附加了其他奇怪的语义。

    如果你真的想知道$1$2 变量是如何工作的,我假设你能找到的唯一“文档”是rubyspec,这是一个由鲁比努斯人。进行愉快的黑客攻击,但要为痛苦做好准备。


    有没有办法通过正确的方式设置 $1, $2 变量从另一个上下文将块传递给 gsub?

    你可以通过以下修改实现你想要的(但我敢打赌你已经知道了)

    require 'pp'
    def hello(z)
      #z = proc {|m| pp $1}
      "hello".gsub(/(o)/, &z)
    end
    z = proc {|m| pp m}
    hello(z)
    

    我不知道有什么方法可以即时更改 proc 的范围。但是你真的想这样做吗?

    【讨论】:

    • 即使在这段代码中 'hello'.gsub(/(e)/) { puts $1 } Proc.new 在 gsub 之前被调用:这就是每种编程语言的工作方式 - 块与其他语言相同。它需要在将它传递到某个地方之前构建。所以我不认为你的回答能解释什么
    • @BogdanGusiev Proc.newgsub 之前被调用,但块不是。它只被解析。块不是参数。
    • 是的,在这两种代码变体中,该块都不会在 gsub 之前调用。因此,不同之处在于将块传递给 gsub 的调用次数:可能是 1 或 2。但它仍然永远不会等于 0。$1 变量永远不会出现在定义 z 的上下文中。
    • @BogdanGusiev $ 变量不像普通变量或全局变量那样工作 - 检查this answer
    • 我在an answer 上发布了关于即时覆盖$~ 的方法。这不漂亮,但可能。
    【解决方案2】:

    $1$2 之类的东西的行为类似于 本地变量,尽管它的前导是 $。你可以试试下面的代码来证明这一点:

    def foo
      /(hell)o/ =~ 'hello'
      $1
    end
    
    def bar
      $1
    end
    
    foo #=> "hell"
    bar #=> nil
    

    你的问题是因为proc z是在方法hello之外定义的,所以zmain的上下文中访问$1,但是gsub在上下文中设置$1方法hello.

    【讨论】:

      【解决方案3】:

      这两个版本是不同的,因为$1 变量是线程局部的和方法局部的。在第一个示例中,$1 仅存在于 hello 方法的 外部 块中。在第二个示例中,$1 存在 hello 方法中。

      无法从方法定义之外将块中的 $1 传递给 gsub。

      注意gsub 将匹配字符串传递到块中,因此z = proc { |m| pp m } 仅在您的正则表达式仅包含整个匹配项时才有效。一旦您的正则表达式包含您想要的引用以外的任何内容,您就不走运了。

      例如,"hello".gsub(/l(o)/) { |m| m } => hello,因为整个匹配字符串都传递给了块。

      "hello".gsub(/l(o)/) { |m| $1 } => helo,因为匹配的l被块丢弃,我们感兴趣的只是捕获的o

      我的解决方案是match这个正则表达式,然后将MatchData对象传入块中:

      require 'pp'
      
      def hello(z)
        string = "hello"
        regex = /(o)/
      
        m = string.match(regex)
        string.gsub(regex, z.call(m))
      end
      
      z = proc { |m| pp m[1] }
      pp hello(z)
      

      【讨论】:

      • 实际上有一些方法可以从外部覆盖 $1 等。这正是 gsub 等首先要做的。确实,使用纯 Ruby 来做这件事很棘手,但并非不可能——请参阅我的 new answer
      【解决方案4】:

      这是一种解决方法(Ruby 2)。给定的 Proc z 的行为与给定给 String#gsub 的块完全相同。

      def hello(z)
        "hello".match /(o)/  # Sets $1, $2, ...
        z.binding.tap do |b|
          b.local_variable_set(:_, $~)
          b.eval("$~=_")
        end
        "hello".gsub(/(o)/, &z)
      end
      z = proc {|m| p $1}
      hello(z)
      # prints: "o"
      

      背景在详细解释 this answer 问题“如何将 Regexp.last_match 传递给 Ruby 中的块”(发布于 2018 年)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2018-12-08
        • 2016-10-06
        • 1970-01-01
        • 1970-01-01
        • 2013-05-16
        相关资源
        最近更新 更多