web-dev-qa-db-fra.com

method_missing attrape en Ruby

Y a-t-il des éléments à prendre en compte lors de la définition de la méthode method_missing dans Ruby? Je me demande s'il existe des interactions moins évidentes entre héritage, lancement d'exceptions, performances ou autre chose.

48
Readonly

Un exemple assez évident: redéfinissez toujours respond_to? si vous redéfinissez method_missing. Si method_missing(:sym) fonctionne, respond_to?(:sym) doit toujours renvoyer true. Il y a beaucoup de bibliothèques qui en dépendent.

Plus tard:

Un exemple:

# Wrap a Foo; don't expose the internal guts.
# Pass any method that starts with 'a' on to the
# Foo.
class FooWrapper
  def initialize(foo)
    @foo = foo
  end
  def some_method_that_doesnt_start_with_a
    'bar'
  end
  def a_method_that_does_start_with_a
    'baz'
  end
  def respond_to?(sym, include_private = false)
    pass_sym_to_foo?(sym) || super(sym, include_private)
  end
  def method_missing(sym, *args, &block)
    return foo.call(sym, *args, &block) if pass_sym_to_foo?(sym)
    super(sym, *args, &block)
  end
  private
  def pass_sym_to_foo?(sym)
    sym.to_s =~ /^a/ && @foo.respond_to?(sym)
  end
end

class Foo
  def argh
    'argh'
  end
  def blech
    'blech'
  end
end

w = FooWrapper.new(Foo.new)

w.respond_to?(:some_method_that_doesnt_start_with_a)
# => true
w.some_method_that_doesnt_start_with_a
# => 'bar'

w.respond_to?(:a_method_that_does_start_with_a)
# => true
w.a_method_that_does_start_with_a
# => 'baz'

w.respond_to?(:argh)
# => true
w.argh
# => 'argh'

w.respond_to?(:blech)
# => false
w.blech
# NoMethodError

w.respond_to?(:glem!)
# => false
w.glem!
# NoMethodError

w.respond_to?(:apples?)
w.apples?
# NoMethodError
59
James A. Rosen

Si votre méthode manquante est uniquement à la recherche de certains noms de méthodes, n'oubliez pas d'appeler super si vous n'avez pas trouvé ce que vous cherchez, pour que les autres méthodes manquantes puissent faire l'affaire.

13
Andrew Grimm

Si vous pouvez anticiper les noms de méthodes, il est préférable de les déclarer dynamiquement que de s'appuyer sur method_missing, car method_missing entraîne une pénalité de performance. Par exemple, supposons que vous vouliez étendre un handle de base de données pour pouvoir accéder aux vues de la base de données avec la syntaxe suivante:

selected_view_rows = @dbh.viewname( :column => value, ... )

Plutôt que de s'appuyer sur method_missing sur le handle de base de données et d'envoyer le nom de la méthode à la base de données en tant que nom d'une vue, vous pouvez déterminer toutes les vues de la base de données à l'avance, puis les parcourir pour créer des méthodes "viewname" sur @dbh. .

11
Pistos

Construire sur Le point de Pistos : method_missing est au moins un ordre de grandeur plus lent que la méthode habituelle appelant toutes les implémentations de Ruby que j'ai essayées. Il a raison d’anticiper dans la mesure du possible d’éviter les appels au method_missing.

Si vous vous sentez aventureux, consultez le peu connu Delegator class de Ruby.

5
James A. Rosen

La réponse de James est excellente, mais dans Ruby moderne (1.9+), comme le dit Marc-André, vous souhaitez redéfinir respond_to_missing? car il vous donne accès à d'autres méthodes au-dessus de respond_to?, comme method(:method_name), ce qui permet de restituer la méthode elle-même.

Exemple, la classe suivante définie:

class UserWrapper
  def initialize
    @json_user = { first_name: 'Jean', last_name: 'Dupont' }
  end

  def method_missing(sym, *args, &block)
    return @json_user[sym] if @json_user.keys.include?(sym)
    super
  end

  def respond_to_missing?(sym, include_private = false)
    @json_user.keys.include?(sym) || super
  end
end

Résulte en:

irb(main):015:0> u = UserWrapper.new
=> #<UserWrapper:0x00007fac7b0d3c28 @json_user={:first_name=>"Jean", :last_name=>"Dupont"}>
irb(main):016:0> u.first_name
=> "Jean"
irb(main):017:0> u.respond_to?(:first_name)
=> true
irb(main):018:0> u.method(:first_name)
=> #<Method: UserWrapper#first_name>
irb(main):019:0> u.foo
NoMethodError (undefined method `foo' for #<UserWrapper:0x00007fac7b0d3c28>)

Donc, définissez toujours respond_to_missing? lors de la substitution de method_missing.

0
Capripot

Un autre gotcha:

method_missing se comporte différemment entre obj.call_method et obj.send(:call_method). Essentiellement, le premier manque toutes les méthodes privées et non définies, tandis que plus tard, il ne manque pas de méthodes privées.

Donc, vous method_missing ne serez jamais dérouté lorsque quelqu'un appelle votre méthode privée via send.

0
jack2684