Làm thế nào để thực hiện một "gọi lại" trong Ruby?


76

Tôi không chắc về thành ngữ hay nhất cho các lệnh gọi lại kiểu C trong Ruby - hoặc liệu có điều gì đó thậm chí còn tốt hơn (và ít giống C hơn). Trong C, tôi sẽ làm một cái gì đó như:

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

Tương đương với Ruby tốt là gì? Về cơ bản, tôi muốn gọi một phương thức được truyền trong lớp, khi một điều kiện nhất định được đáp ứng trong "DoStuff"


Điều này có thể hữu ích: github.com/krisleech/wisper
Kris

Câu trả lời:


97

Tương đương với ruby, không phải là thành ngữ, sẽ là:

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

Cách tiếp cận thành ngữ sẽ là truyền một khối thay vì tham chiếu đến một phương thức. Một lợi thế của một khối so với phương thức tự do là ngữ cảnh - một khối là một bao đóng , vì vậy nó có thể tham chiếu đến các biến từ phạm vi mà nó được khai báo. Điều này cắt giảm số lượng tham số do_stuff cần phải chuyển đến lệnh gọi lại. Ví dụ:

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

17
Nếu bạn đang sử dụng lợi nhuận, bạn không cần & chặn trong danh sách đối số.
Douglas

41
Tôi vẫn thích sử dụng &blockký hiệu vì sau đó rõ ràng là phương thức này nhận một khối chỉ bằng cách nhìn vào dòng đầu tiên của định nghĩa.
Patrick Oscity

đồng ý với nhận xét của @Douglas; & block khiến tôi thật kỳ lạ :(
gabriel-kaam

80

"Khối thành ngữ" này là một phần cốt lõi của Ruby hàng ngày và được đề cập thường xuyên trong các sách và hướng dẫn. Phần thông tin Ruby cung cấp các liên kết đến các nguồn học tập [trực tuyến] hữu ích.


Cách thành ngữ là sử dụng một khối:

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

Hoặc có lẽ được chuyển đổi thành Proc ; ở đây tôi cho thấy rằng "khối", được chuyển đổi ngầm thành Proc với &block, chỉ là một giá trị "có thể gọi" khác:

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

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

(Chỉ sử dụng biểu mẫu trên để lưu block-now-Proc để sử dụng sau này hoặc trong các trường hợp đặc biệt khác vì nó thêm nhiễu cú pháp và chi phí).

Tuy nhiên, một lambda có thể được sử dụng dễ dàng (nhưng điều này không phải là thành ngữ):

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

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

Mặc dù tất cả các phương pháp ở trên đều có thể bao bọc "việc gọi một phương thức" khi chúng tạo ra các bao đóng, các Phương thức liên kết cũng có thể được coi là các đối tượng có thể gọi cấp một:

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

Ngoài ra, đôi khi sẽ hữu ích khi sử dụng #sendphương thức (đặc biệt nếu một phương thức được biết đến theo tên). Ở đây nó lưu một đối tượng Phương thức trung gian đã được tạo trong ví dụ trước; Ruby là một hệ thống truyền thông điệp:

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

Chúc bạn viết mã vui vẻ!


6

Đã khám phá chủ đề nhiều hơn một chút và cập nhật mã.

Phiên bản sau là một nỗ lực để tổng quát hóa kỹ thuật, mặc dù vẫn được đơn giản hóa và chưa hoàn thiện.

Phần lớn tôi đã đánh cắp - hem, đã tìm thấy cảm hứng - việc triển khai các lệnh gọi lại của DataMapper, đối với tôi, nó có vẻ khá hoàn chỉnh và đẹp mắt.

Tôi thực sự khuyên bạn nên xem mã @ http://github.com/datamapper/dm-core/blob/master/lib/dm-core/support/hook.rb

Dù sao, cố gắng tái tạo chức năng bằng mô-đun Observable khá hấp dẫn và mang tính hướng dẫn. Một số lưu ý:

  • Phương thức được thêm vào dường như được yêu cầu vì các phương thức phiên bản gốc không khả dụng tại thời điểm đăng ký các lệnh gọi lại
  • lớp bao gồm cả được quan sát và tự quan sát
  • ví dụ được giới hạn cho các phương thức cá thể, không hỗ trợ khối, args, v.v.

mã:

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...

Bản trình bày đơn giản này về việc sử dụng Observable có thể hữu ích: http://www.oreillynet.com/ruby/blog/2006/01/ruby_design_patterns_observer.html


Tôi hy vọng bạn không phiền nhưng tôi đã viết mã của bạn và làm lại nó một chút cho dự án hiện tại của tôi. Hiện tại nó rất đơn giản nhưng bạn có thể thoải mái phê bình / xỉa xói
Dave Sims

Một câu hỏi tôi đã làm việc với điều này - tại sao bạn sử dụng Observable? Tôi có một cuộc đụng độ tên phương thức với lớp mà tôi cần tích hợp và chỉ cần gọi một phương thức thể hiện (trigger_callbacks) đã hoạt động tốt.
Dave Sims

3

Vì vậy, điều này có thể rất "không đẹp mắt", và tôi không phải là một nhà phát triển Ruby "chuyên nghiệp", vì vậy nếu các bạn có ý định đánh lừa, hãy nhẹ nhàng làm ơn :)

Ruby có một mô-đun tích hợp được gọi là Observer. Tôi không thấy nó dễ sử dụng, nhưng công bằng mà nói, tôi đã không cho nó nhiều cơ hội. Trong các dự án của mình, tôi đã sử dụng đến việc tạo loại EventHandler của riêng mình (vâng, tôi sử dụng C # rất nhiều). Đây là cấu trúc cơ bản:

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

Vì vậy, để sử dụng điều này, tôi hiển thị nó như một thành viên chỉ đọc của một lớp:

class Foo

  attr_reader :some_value_changed

  def initialize
    @some_value_changed = EventHandler.new
  end

end

Khách hàng của lớp "Foo" có thể đăng ký một sự kiện như sau:

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

Tôi chắc chắn đây không phải là Ruby thành ngữ và tôi chỉ đang đúc kết kinh nghiệm C # của mình sang một ngôn ngữ mới, nhưng nó đã hiệu quả với tôi.


1

Nếu bạn sẵn sàng sử dụng ActiveSupport (từ Rails), bạn có một triển khai đơn giản

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

0

Tôi thường triển khai các lệnh gọi lại trong Ruby như trong ví dụ sau. Nó rất thoải mái để sử dụng.

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

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.