2008-11-14 3 views
46

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

57

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 
+0

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)? –

+0

@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'. –

+16

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 –

9

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.

5

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.

11

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.

+1

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

0

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.

0

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.