【问题标题】:How to implement a "callback" in Ruby?如何在 Ruby 中实现“回调”?
【发布时间】:2010-12-13 06:24:59
【问题描述】:

我不确定 Ruby 中 C 风格回调的最佳习语——或者是否有更好的东西(而不像 C 那样)。在 C 中,我会这样做:

void DoStuff( int parameter, CallbackPtr callback )
{
  // Do stuff
  ...
  // Notify we're done
  callback( status_code )
}

什么是好的 Ruby 等价物?本质上,我想在“DoStuff”中满足某个条件时调用传入的类方法

【问题讨论】:

标签: ruby callback


【解决方案1】:

这个“惯用块”是日常 Ruby 的核心部分,经常在书籍和教程中介绍。Ruby information section 提供了有用的[在线]学习资源的链接。


惯用的方式是使用块:

def x(z)
  yield z   # perhaps used in conjunction with #block_given?
end
x(3) {|y| y*y}  # => 9

或者可能转换为Proc;在这里,我展示了用&block 隐式转换为 Proc 的“块”只是另一个“可调用”值:

def x(z, &block)
  callback = block
  callback.call(z)
end

# look familiar?
x(4) {|y| y * y} # => 16

(仅使用上述形式保存 block-now-Proc 以供以后使用或在其他特殊情况下使用,因为它会增加开销和语法噪音。)

不过,也可以轻松地使用 lambda(但这不是惯用的):

def x(z,fn)
  fn.call(z)
end

# just use a lambda (closure)
x(5, lambda {|y| y * y}) # => 25

虽然上述方法都可以在创建闭包时包装“调用方法”,但绑定的Methods 也可以被视为一流的可调用对象:

class A
  def b(z)
    z*z
  end
end

callable = A.new.method(:b)
callable.call(6) # => 36

# and since it's just a value...
def x(z,fn)
  fn.call(z)
end
x(7, callable) # => 49

此外,有时使用#send 方法很有用(特别是在方法名称已知的情况下)。这里它保存了在上一个示例中创建的中间 Method 对象; Ruby 是一个消息传递系统:

# Using A from previous
def x(z, a):
  a.__send__(:b, z)
end
x(8, A.new) # => 64

编码愉快!

【讨论】:

    【解决方案2】:

    所以,这可能是非常“非红宝石”,而且我不是一个“专业”的 Ruby 开发人员,所以如果你们要打,请温柔:)

    Ruby 有一个名为 Observer 的内置模块。我还没有发现它易于使用,但公平地说,我没有给它太多机会。在我的项目中,我求助于创建自己的 EventHandler 类型(是的,我经常使用 C#)。这是基本结构:

    class EventHandler
    
      def initialize
        @client_map = {}
      end
    
      def add_listener(id, func)
        (@client_map[id.hash] ||= []) << func
      end
    
      def remove_listener(id)
        return @client_map.delete(id.hash)
      end
    
      def alert_listeners(*args)
        @client_map.each_value { |v| v.each { |func| func.call(*args) } }
      end
    
    end
    

    因此,为了使用它,我将其公开为类的只读成员:

    class Foo
    
      attr_reader :some_value_changed
    
      def initialize
        @some_value_changed = EventHandler.new
      end
    
    end
    

    “Foo”类的客户端可以订阅这样的事件:

    foo.some_value_changed.add_listener(self, lambda { some_func })
    

    我确信这不是惯用的 Ruby,我只是将我的 C# 经验硬塞进一门新语言中,但它对我有用。

    【讨论】:

      【解决方案3】:

      不是惯用的 ruby​​ 等价物是:

      def my_callback(a, b, c, status_code)
        puts "did stuff with #{a}, #{b}, #{c} and got #{status_code}"
      end
      
      def do_stuff(a, b, c, callback)
        sum = a + b + c
        callback.call(a, b, c, sum)
      end
      
      def main
        a = 1
        b = 2
        c = 3
        do_stuff(a, b, c, method(:my_callback))
      end
      

      惯用的方法是传递一个块而不是对方法的引用。块相对于独立方法的一个优势是上下文 - 块是closure,因此它可以引用声明它的范围内的变量。这减少了 do_stuff 需要传递给回调的参数数量。例如:

      def do_stuff(a, b, c, &block)
        sum = a + b + c
        yield sum
      end
      
      def main
        a = 1
        b = 2
        c = 3
        do_stuff(a, b, c) { |status_code|
          puts "did stuff with #{a}, #{b}, #{c} and got #{status_code}"
        }
      end
      

      【讨论】:

      • 如果你使用yield,你不需要在参数列表中使用&block。
      • 我仍然喜欢使用&amp;block 表示法,因为只要查看定义的第一行,就可以清楚地看出该方法占用了一个块。
      • 同意@Douglas 评论; &block 让我很奇怪 :(
      【解决方案4】:

      我经常在 Ruby 中实现回调,如下例所示。用起来很舒服。

      class Foo
         # Declare a callback.
         def initialize
           callback( :on_die_cast )
         end
      
         # Do some stuff.
         # The callback event :on_die_cast is triggered.
         # The variable "die" is passed to the callback block.
         def run
            while( true )
               die = 1 + rand( 6 )
               on_die_cast( die )
               sleep( die )
            end
         end
      
         # A method to define callback methods.
         # When the latter is called with a block, it's saved into a instance variable.
         # Else a saved code block is executed.
         def callback( *names )
            names.each do |name|
               eval <<-EOF
                  @#{name} = false
                  def #{name}( *args, &block )
                     if( block )
                        @#{name} = block
                     elsif( @#{name} )
                        @#{name}.call( *args )
                     end
                  end
               EOF
            end
         end
      end
      
      foo = Foo.new
      
      # What should be done when the callback event is triggered?
      foo.on_die_cast do |number|
         puts( number )
      end
      
      foo.run
      

      【讨论】:

        【解决方案5】:

        进一步探索了主题并更新了代码。

        以下版本是对该技术进行概括的尝试,尽管仍然极其简化和不完整。

        我主要偷了 - 哼哼,在 - DataMapper 回调的实现中找到了灵感,这在我看来是相当完整和美丽的。

        强烈建议看一下代码@http://github.com/datamapper/dm-core/blob/master/lib/dm-core/support/hook.rb

        无论如何,尝试使用 Observable 模块重现该功能非常吸引人且具有启发性。 几点说明:

        • 添加的方法似乎是必需的,因为在注册回调时原始实例方法不可用
        • 包含类既是观察者又是自我观察者
        • 示例仅限于实例方法,不支持blocks、args等

        代码:

        require 'observer'
        
        module SuperSimpleCallbacks
          include Observable
        
          def self.included(klass)
            klass.extend ClassMethods
            klass.initialize_included_features
          end
        
          # the observed is made also observer
          def initialize
            add_observer(self)
          end
        
          # TODO: dry
          def update(method_name, callback_type) # hook for the observer
            case callback_type
            when :before then self.class.callbacks[:before][method_name.to_sym].each{|callback| send callback}
            when :after then self.class.callbacks[:after][method_name.to_sym].each{|callback| send callback}
            end
          end
        
          module ClassMethods
            def initialize_included_features
              @callbacks = Hash.new
              @callbacks[:before] = Hash.new{|h,k| h[k] = []}
              @callbacks[:after] = @callbacks[:before].clone
              class << self
                attr_accessor :callbacks
              end
            end
        
            def method_added(method)
              redefine_method(method) if is_a_callback?(method)
            end
        
            def is_a_callback?(method)
              registered_methods.include?(method)
            end
        
            def registered_methods
              callbacks.values.map(&:keys).flatten.uniq
            end
        
            def store_callbacks(type, method_name, *callback_methods)
              callbacks[type.to_sym][method_name.to_sym] += callback_methods.flatten.map(&:to_sym)
            end
        
            def before(original_method, *callbacks)
              store_callbacks(:before, original_method, *callbacks)
            end
        
            def after(original_method, *callbacks)
              store_callbacks(:after, original_method, *callbacks)
            end
        
            def objectify_and_remove_method(method)
              if method_defined?(method.to_sym)
                original = instance_method(method.to_sym)
                remove_method(method.to_sym)
                original
              else
                nil
              end
            end
        
            def redefine_method(original_method)
              original = objectify_and_remove_method(original_method)
              mod = Module.new
              mod.class_eval do
                define_method(original_method.to_sym) do
                  changed; notify_observers(original_method, :before)
                  original.bind(self).call if original
                  changed; notify_observers(original_method, :after)
                end
              end
              include mod
            end
          end
        end
        
        
        class MyObservedHouse
          include SuperSimpleCallbacks
        
          before :party, [:walk_dinosaure, :prepare, :just_idle]
          after :party, [:just_idle, :keep_house, :walk_dinosaure]
        
          before :home_office, [:just_idle, :prepare, :just_idle]
          after :home_office, [:just_idle, :walk_dinosaure, :just_idle]
        
          before :second_level, [:party]
        
          def home_office
            puts "learning and working with ruby...".upcase
          end
        
          def party
            puts "having party...".upcase
          end
        
          def just_idle
            puts "...."
          end
        
          def prepare
            puts "preparing snacks..."
          end
        
          def keep_house
            puts "house keeping..."
          end
        
          def walk_dinosaure
            puts "walking the dinosaure..."
          end
        
          def second_level
            puts "second level..."
          end
        end
        
        MyObservedHouse.new.tap do |house|
          puts "-------------------------"
          puts "-- about calling party --"
          puts "-------------------------"
        
          house.party
        
          puts "-------------------------------"
          puts "-- about calling home_office --"
          puts "-------------------------------"
        
          house.home_office
        
          puts "--------------------------------"
          puts "-- about calling second_level --"
          puts "--------------------------------"
        
          house.second_level
        end
        # => ...
        # -------------------------
        # -- about calling party --
        # -------------------------
        # walking the dinosaure...
        # preparing snacks...
        # ....
        # HAVING PARTY...
        # ....
        # house keeping...
        # walking the dinosaure...
        # -------------------------------
        # -- about calling home_office --
        # -------------------------------
        # ....
        # preparing snacks...
        # ....
        # LEARNING AND WORKING WITH RUBY...
        # ....
        # walking the dinosaure...
        # ....
        # --------------------------------
        # -- about calling second_level --
        # --------------------------------
        # walking the dinosaure...
        # preparing snacks...
        # ....
        # HAVING PARTY...
        # ....
        # house keeping...
        # walking the dinosaure...
        # second level...
        

        这个 Observable 使用的简单介绍可能很有用:http://www.oreillynet.com/ruby/blog/2006/01/ruby_design_patterns_observer.html

        【讨论】:

        • 我希望你不介意,但我抄袭了你的代码,并为我当前的项目重新编写了一些代码。目前它很简单,但你可以去 - 随时批评/抨击等。github.com/davesims/Simple-AOP/blob/master/lib/simple_aop.rb
        • 我遇到的一个问题——你为什么使用 Observable?我的方法名称与我需要集成的类发生冲突,只需调用实例方法(trigger_callbacks)就可以了。
        【解决方案6】:

        我知道这是一篇旧帖子,但遇到此问题的其他人可能会发现我的解决方案很有帮助。

        http://chrisshepherddev.blogspot.com/2015/02/callbacks-in-pure-ruby-prepend-over.html

        【讨论】:

          【解决方案7】:

          如果你愿意使用ActiveSupport(来自 Rails),你有一个简单的实现

          class ObjectWithCallbackHooks
            include ActiveSupport::Callbacks
            define_callbacks :initialize # Your object supprots an :initialize callback chain
          
            include ObjectWithCallbackHooks::Plugin 
          
            def initialize(*)
              run_callbacks(:initialize) do # run `before` callbacks for :initialize
                puts "- initializing" # then run the content of the block
              end # then after_callbacks are ran
            end
          end
          
          module ObjectWithCallbackHooks::Plugin
            include ActiveSupport::Concern
          
            included do 
              # This plugin injects an "after_initialize" callback 
              set_callback :initialize, :after, :initialize_some_plugin
            end
          end
          

          【讨论】:

            【解决方案8】:

            我知道这是一篇旧帖子,但我在尝试解决类似问题时发现了它。

            这是一个非常优雅的解决方案,最重要的是,它可以在有或没有回调的情况下工作。


            假设我们有一个 Arithmetic 类,它实现了对它们的基本操作 - additionsubtraction

            class Arithmetic
              def addition(a, b)
                a + b
              end
            
              def subtraction(a, b)
                a - b
              end
            end
            

            我们想为每个操作添加一个回调,它会对输入数据和结果做一些事情。

            在下面的示例中,我们将实现 after_operation 方法,该方法接受将在操作后执行的 Ruby 块。

            class Arithmetic
              def after_operation(&block)
                @after_operation_callback = block
              end
            
              def addition(a, b)
                do_operation('+', a, b)
              end
            
              def subtraction(a, b)
                do_operation('-', a, b)
              end
            
              private
            
              def do_operation(sign, a, b)
                result =
                  case sign
                  when '+'
                    a + b
                  when '-'
                    a - b
                  end
            
                if callback = @after_operation_callback
                  callback.call(sign, a, b, result)
                end
            
                result
              end
            end
            

            与回调一起使用:

            callback = -> (sign, a, b, result) do
              puts "#{a} #{sign} #{b} = #{result}"
            end
            
            arithmetic = Arithmetic.new
            arithmetic.after_operation(&callback)
            
            puts arithmetic.addition(1, 2)
            puts arithmetic.subtraction(3, 1)
            

            输出:

            1 + 2 = 3
            3
            3 - 1 = 2
            2
            

            【讨论】:

              猜你喜欢
              • 2010-09-08
              • 1970-01-01
              • 2017-04-25
              • 2016-04-10
              • 2018-01-06
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多