Y at-il des choses à faire attention lors de la définition de la méthode method_missing
dans Ruby? Je me demande s'il y a des interactions pas si évidentes de l'héritage, du lancement d'exception, de la performance ou de n'importe quoi d'autre.method_missing gotchas dans Ruby
Répondre
Un peu évident: toujours redéfinir 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
Si vous pouvez anticiper les noms de méthode, il est préférable de les déclarer dynamiquement que de compter sur method_missing parce que method_missing encourt une pénalité de performance. Par exemple, supposons que vous vouliez étendre une poignée de base de données pour pouvoir accéder à des vues de base de données avec cette syntaxe:
selected_view_rows = @dbh.viewname(:column => value, ...)
Plutôt que de compter sur method_missing sur la poignée de base de données et l'envoi du nom de la méthode à la base de données comme le nom de Dans une vue, vous pouvez déterminer toutes les vues de la base de données à l'avance, puis parcourir celles-ci pour créer des méthodes "viewname" sur @dbh.
S'appuyant sur Pistos's point: method_missing
est au moins d'un ordre de grandeur plus lent que la méthode normale appelant toutes les implémentations Ruby que j'ai essayées. Il a raison d'anticiper si possible pour éviter les appels à method_missing
.
Si vous vous sentez aventureux, jetez un oeil à la classe Delegator peu connue de Ruby.
Si votre méthode manquante de méthode ne recherche que certains noms de méthodes, n'oubliez pas d'appeler super si vous n'avez pas trouvé ce que vous cherchez, afin que d'autres manquements de méthode puissent faire leur truc.
Oui, sinon votre appel de méthode échouera silencieusement et vous passerez des heures à essayer de comprendre pourquoi votre méthode ne fonctionne pas, même s'il n'y a pas d'erreur. (pas que j'aurais fait une telle chose) – PhillipKregg
Une 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 on ne manque pas de méthodes privées.
Donc, vous method_missing
ne piège jamais l'appel lorsque quelqu'un appelle votre méthode privée via send
.
La réponse de James est grande mais, en rubis moderne (1.9+), comme Marc-André dit, vous voulez redéfinir respond_to_missing?
car il vous donne accès à d'autres méthodes au-dessus de respond_to?
, comme method(:method_name)
retour de 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ésultats dans:
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>)
Ainsi, toujours définir respond_to_missing?
lors de la substitution method_missing
.
C'est intéressant.Comment l'implémenteriez-vous pour une classe constituée de méthodes "normales" et de méthodes "dynamiques" (implémentées via method_missing)? –
@Christoph: Votre méthode 'pass_sym_to_foo?' Devient une méthode générique 'handle?' Qui décide d'essayer de traiter cette requête ou de la transmettre à 'super_'' method_missing'. –
Dans Ruby 1.9.2, il est encore mieux de redéfinir 'respond_to_missing?', Voir mon article de blog: http://blog.marc-andre.ca/2010/11/methodmissing-politely.html –