【问题标题】:Is there a way to redefine a method within a block?有没有办法重新定义块内的方法?
【发布时间】:2019-04-30 20:29:18
【问题描述】:

假设我想puts大量字符串大写,但为了简洁,我想每次都写puts(string)而不是puts(string.upcase)

有没有办法让我重新定义puts仅在一个块内,像这样?

# This will puts as normal
puts "abc"
# => abc

# This will puts an upcased string, but it's too verbose
puts "abc".upcase
# => ABC

# I want to do something like this, which will override the puts method for code run within the block
def always_upcase_strings(&block)
  def puts(string)
    super(string.upcase)
  end
end

always_upcase_strings do 
  puts "abc"
  puts "def"
  puts "ghi"
end
puts "xyz"
# => ABC
# => DEF
# => GHI
# => xyz

我上面的例子被简化了——在我的例子中我没有使用puts,而是我自己编写的一个方法。

【问题讨论】:

    标签: ruby


    【解决方案1】:

    second thoughts,这里有一个更好的答案:

    def always_upcase_strings(&block)
      anon_class = Class.new do
        def puts(str)
          super(str.upcase)
        end
      end
      anon_class.new.instance_eval(&block)
    end
    
    always_upcase_strings do
      puts "abc" #=> "ABC"
      puts "def" #=> "DEF"
      puts "ghi" #=> "GHI"
    end
    
    puts "xyz"   #=> "xyz"
    

    这个creates 是一个临时类(匿名,因为它不需要名称),具有所需的方法覆盖。

    然后调用生成的块,在此类实例的上下文中

    除了比我的“重新定义-取消定义-重新定义方法”解决方案更容易混淆之外,这种方法还具有线程安全的额外优势。因此,例如,如果在调用方法的同时运行并行测试,您将不会出现奇怪的行为。

    ...但是我仍然坚持我最初的说法,即在一个块中重新定义方法是非常令人惊讶的行为。同事可能不喜欢你选择这种设计模式(除非它以有限的方式完成,并且有充分的理由!)

    【讨论】:

    • 它甚至可以在没有类的情况下工作:obj = Object.new,然后是 def obj.puts ...,最后是 obj.instance_eval(&block) 然而,instance_eval 方法的缺点是 self 将在块内发生变化,这可能打破包装的代码。使用method_missing 将未覆盖的方法委托给原始接收者可以工作,但随后又变得复杂了。
    • 与第一种方法相比,我更喜欢这样的方法(并回应您的所有免责声明)。这个instance_eval 的东西是 FactoryBot 如何获得他们干净的 DSL。 github.com/thoughtbot/factory_bot/blob/…
    【解决方案2】:

    这有点疯狂;我不建议在生产中运行这样的东西......但我很好奇,并找到了一种使用 ruby​​ 元编程的方法:

    def always_upcase_strings(&block)
      original_puts = method(:puts)
      define_method(:puts) do |string|
        original_puts.call(string.upcase)
      end
      yield
      undef :puts
      define_method(:puts, original_puts.unbind)
    end
    
    always_upcase_strings do
      puts "abc" #=> "ABC"
      puts "def" #=> "DEF"
      puts "ghi" #=> "GHI"
    end
    
    puts "xyz"   #=> "xyz"
    

    这是怎么回事?

    • 首先,我获取puts Method object,并将其存储在一个变量中。
    • 然后,使用所需的修改将puts 重新定义为invoke it。 (您也可以使用 super(string.upcase) 来执行此操作,但我认为上面的内容更好地说明了正在发生的事情。)
    • 然后yield到一个块,这样你就可以调用新版本的方法了。
    • 接下来,undef 方法 (!!!),以完全摆脱覆盖。 (但不幸的是,这也删除了原始方法。所以......)
    • 最后,re-define the puts methodre-binding the orignalself

    这绝对是黑魔法。使用后果自负。


    我的“现实世界”建议是:

    def puts_upcase(str)
      puts str.upcase
    end
    
    puts_upcase "abc" #=> "ABC"
    puts "xyz"        #=> "xyz"
    

    或者在你的场景中,如果你真的想调整类方法的行为,也许Module#prepend 是一个更好的工具。

    更简单的解决方案往往是更好的解决方案。

    【讨论】:

    • 你真的需要undef :putsunbind 的电话吗?只需调用define_method(:puts, original_puts) 恢复原来的方法似乎就足够了。
    • 是的,你可以。除非这确实意味着每次调用方法时都会用越来越多的 put 定义“污染”内核。
    • 你呢?我认为它会简单地替换以前的定义。
    猜你喜欢
    • 1970-01-01
    • 2021-01-01
    • 1970-01-01
    • 2021-04-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-12
    • 2012-07-31
    相关资源
    最近更新 更多