【问题标题】:Can dynamic method in outer block be accessed in child block? - Ruby Meta Programming外部块中的动态方法可以在子块中访问吗? - Ruby 元编程
【发布时间】:2023-04-03 21:55:02
【问题描述】:

我有一个允许我动态编写 ruby​​ 代码的 DSL。

Outer 类采用要处理的自定义代码块。

还有一种众所周知的 DSL 方法称为 settings,它可以采用自己的代码块进行配置。

我希望能够在另一个块中创建可重用的方法,并让它们在内部块中可用。

在为这篇文章编写示例代码时,我偶然发现了一种用法,它通过将 self 分配给外部作用域中的 variable 并在子作用域中调用 variable 上的方法。

我宁愿不需要将 self 分配给变量,我注意到如果我尝试在 RSPEC 中做类似的事情,那么我不需要使用 variable = self,我可以在父块,它们在子块中可用,请参见最后一个示例。

class Settings
  attr_accessor :a
  attr_accessor :b
  attr_accessor :c

  def initialize(&block)
    instance_eval(&block)
  end
end

class Outer
  def initialize(&block)
    instance_eval(&block)
  end

  def build_settings(&block)
    Settings.new(&block)
  end
end

运行代码

Outer.new do

  # Create a method dynamically in the main block
  def useful_method
    '** result of the useful_method **'
  end

  x = self

  settings = build_settings do
    self.a = 'aaaa'
    self.b = useful_method()    # Throws exception
    self.c = x.useful_method()  # Works
  end

end

运行代码(带有详细的日志记录)

# Helper to colourize the console log
class String
  def error;          "\033[31m#{self}\033[0m" end
  def success;        "\033[32m#{self}\033[0m" end
end

# Run code with detailed logging
Outer.new do

  # Create a method dynamically in the main block
  def useful_method
    '** result of the useful_method **'
  end

  puts "respond?: #{respond_to?(:useful_method).to_s.success}"

  x = self

  settings = build_settings do
    puts "respond?: #{respond_to?(:useful_method).to_s.error}"
    self.a = 'aaaa'
    begin
      self.b = useful_method().success
    rescue
      self.b = 'bbbb'.error
    end
    begin
      self.c = x.useful_method().success
    rescue
      self.c = 'cccc'.error
    end
  end

  puts "a: #{settings.a}"
  puts "b: #{settings.b}"
  puts "c: #{settings.c}"

end

来自运行代码的控制台日志

  • 分配给b 会引发异常
  • 分配给c 工作正常

RSpec 中不需要指定self 的示例

为什么我可以访问 RSpec DSL 中的有用方法,但不能访问我自己的。

RSpec.describe 'SomeTestSuite' do
  context 'create method in this outer block' do
    def useful_method
      'david'
    end

    it 'use outer method in this inner block' do
      expect(useful_method).to eq('david')
    end
  end
end

【问题讨论】:

    标签: ruby


    【解决方案1】:

    您可以将Outer 实例传递给Settings.new

    class Outer
      def initialize(&block)
        instance_eval(&block)
      end
    
      def build_settings(&block)
        Settings.new(self, &block)
        #            ^^^^
      end
    end
    

    Settings 中使用method_missing 将未定义的方法调用委托给outer

    class Settings
      attr_accessor :a
      attr_accessor :b
      attr_accessor :c
    
      def initialize(outer, &block)
        @outer = outer
        instance_eval(&block)
      end
    
      private
    
      def method_missing(name, *args, &block)
        return super unless @outer.respond_to?(name)
    
        @outer.public_send(name, *args, &block)
      end
    
      def respond_to_missing?(name, include_all = false)
        @outer.respond_to?(name, include_all) or super
      end
    end
    

    这样,useful_method 可以在没有明确接收者的情况下被调用。

    【讨论】:

    • 很好,我明天试试
    【解决方案2】:

    您可以为此使用方法Forwardable#def_delegator

    我们首先创建OuterSettings 类。

    class Outer
      attr_reader :settings
    
      def initialize(&block)
        instance_eval(&block)
      end
    
      def build_settings(&block)
        @settings = Settings.new(&block)
      end
    end
    
    class Settings
      attr_accessor :a
      attr_accessor :b
      attr_accessor :c
    
      def initialize(&block)
        instance_eval(&block)
      end
    end
    

    我在Outer 中包含了一个实例变量@settings 来保存Settings 的一个实例,该实例将由Outer#build_settings 动态创建。

    我们现在创建一个带有块的Outer 实例。

    require 'forwardable'
    
    outer = Outer.new do
      def useful_method
        '** result of the useful_method **'
      end
    
      Settings.extend Forwardable
      Settings.public_send(:attr_accessor, :outer)
      Settings.public_send(:def_delegator, :@outer, :useful_method)
      x = self
    
      settings = build_settings do
        self.outer = x
        self.a = 'aaaa'
        self.b = useful_method
      end
    end
      #=> #<Outer:0x00007ffa6f9da320 @settings=#<Settings:0x00007ffa6f9d8318
      #     @a="aaaa", @outer=#<Outer:0x00007ffa6f9da320 ...>,
      #     @b="** result of the useful_method **">>
    

    如您所见,以下操作由块执行。

    • 实例方法Outer#useful_method已创建
    • extend 用于将 Forwardable 包含在 Settings' 单例类中
    • @outer 的读写访问器在 Settings 中创建
    • Settings 中对实例方法useful_method 的调用被委托给@outer 的值,从而导致Outer#useful_method 被调用
    • 创建Settings 的实例,并初始化其实例变量@outer@a@b,将@outer 设置为等于刚刚创建的Outer 的实例。李>

    我们现在可以检索刚刚创建的Settings 的实例并检查其实例变量的值。

    settings = outer.settings
      #=> #<Settings:0x00007ffa6f9d8318 @a="aaaa",
      #     @outer=#<Outer:0x00007ffa6f9da320
      #     @settings=#<Settings:0x00007ffa6f9d8318 ...>>,
      #     @b="** result of the useful_method **">  
    
    settings.outer
      #=> #<Outer:0x00007ffa6f9da320 @settings=#<Settings:0x00007ffa6f9d8318
      #     @a="aaaa", @outer=#<Outer:0x00007ffa6f9da320 ...>,
      #     @b="** result of the useful_method **">>
    settings.a
      #=> "aaaa" 
    settings.b
      #=> "** result of the useful_method **" 
    

    【讨论】:

      猜你喜欢
      • 2013-04-27
      • 1970-01-01
      • 2010-12-08
      • 1970-01-01
      • 2012-03-19
      • 1970-01-01
      • 2015-03-08
      • 2011-11-25
      • 1970-01-01
      相关资源
      最近更新 更多