2016-01-12 56 views
5

All'interno di un metodo in fase di esecuzione, esiste un modo per sapere se tale metodo è stato chiamato tramite super in una sottoclasse? Per esempio.L'attuale metodo Ruby viene chiamato tramite super?

module SuperDetector 
    def via_super? 
    # what goes here? 
    end 
end 

class Foo 
    include SuperDetector 

    def bar 
    via_super? ? 'super!' : 'nothing special' 
    end 
end 

class Fu < Foo 
    def bar 
    super 
    end 
end 

Foo.new.bar # => "nothing special" 
Fu.new.bar # => "super!" 

Come potrei scrivere via_super?, o, se necessario, via_super?(:bar)?

risposta

1

L'ultimo mix tra my other, @mudasobwa's e @sawa's risposte più rec Supporto ursion:

module SuperDetector 
    def self.included(clazz) 
    unless clazz.instance_methods.include?(:via_super?) 
     clazz.send(:define_method, :via_super?) do 
     first_caller_location = caller_locations.first 
     calling_method = first_caller_location.base_label 

     same_origin = ->(other_location) do 
      first_caller_location.lineno == other_location.lineno and 
      first_caller_location.absolute_path == other_location.absolute_path 
     end 

     location_changed = false 
     same_name_stack = caller_locations.take_while do |location| 
      should_take = location.base_label == calling_method and !location_changed 
      location_changed = !same_origin.call(location) 
      should_take 
     end 

     self.kind_of?(clazz) and !same_origin.call(same_name_stack.last) 
     end 
    end 
    end 
end 

L'unico caso che non funzionerà (per quanto ne so) è se si dispone di ricorsione indiretta nella classe base, ma non ho idee su come gestire la cosa con qualsiasi cosa corto di analisi del codice.

4

v'è probabilmente un modo migliore, ma l'idea generale è che Object#instance_of? è limitato solo alla classe corrente, piuttosto che la gerarchia:

module SuperDetector 
    def self.included(clazz) 
    clazz.send(:define_method, :via_super?) do 
     !self.instance_of?(clazz) 
    end 
    end 
end 

class Foo 
    include SuperDetector 

    def bar 
    via_super? ? 'super!' : 'nothing special' 
    end 
end 

class Fu < Foo 
    def bar 
    super 
    end 
end 

Foo.new.bar # => "nothing special" 
Fu.new.bar # => "super!" 


Si noti tuttavia che questo non richiede esplicita super nel bambino. Se il bambino non ha un tale metodo e viene utilizzato quello del genitore, via_super? restituirà comunque true. Non penso che ci sia un modo per prendere solo il caso super oltre a controllare la traccia dello stack o il codice stesso.

+0

Sfortunatamente questo non funziona se entrambi, genitore e figlio includono 'SuperDetector'. – Stefan

+0

@Stefan 'a meno che instance_methods.include?' – mudasobwa

+0

@mudasobwa che impedirebbe al bambino di usare' SuperDetector' – Stefan

3

Un addendum ad un ottimo approccio @ndn:

module SuperDetector 
    def self.included(clazz) 
    clazz.send(:define_method, :via_super?) do 
     self.ancestors[1..-1].include?(clazz) && 
     caller.take(2).map { |m| m[/(?<=`).*?(?=')/] }.reduce(&:==) 
     # or, as by @ndn: caller_locations.take(2).map(&:label).reduce(&:==) 
    end unless clazz.instance_methods.include? :via_super? 
    end 
end 

class Foo 
    include SuperDetector 

    def bar 
    via_super? ? 'super!' : 'nothing special' 
    end 
end 

class Fu < Foo 
    def bar 
    super 
    end 
end 

puts Foo.new.bar # => "nothing special" 
puts Fu.new.bar # => "super!" 

Qui usiamo Kernel#caller fare in modo che il nome del metodo chiamato corrisponde al nome della classe eccellente. Questo approccio richiede probabilmente una messa a punto aggiuntiva in caso di discendente non diretto (caller(2) dovrebbe essere modificato per analisi più sofisticate), ma probabilmente si ottiene il punto.

UPD grazie a @ commento di Stefan per l'altra risposta, aggiornato con unless defined per farlo funzionare quando entrambi Foo e Fuinclude SuperDetector.

UPD2 utilizzando gli antenati per verificare il confronto super anziché normale.

+0

Praticamente - sì. Basta usare 'Kernel # caller_locations' e richiamare' location' su di essi, piuttosto che analizzare con espressioni regolari. – ndn

+0

@ndn intendevi chiamare 'label', giusto? Non vedo alcun vantaggio, ma aggiornato :) – mudasobwa

+0

Sì, 'label'. Colpa mia. È solo più facile da leggere. – ndn

2

Modifica Migliorato, seguendo il suggerimento di Stefan.

module SuperDetector 
    def via_super? 
    m0, m1 = caller_locations[0].base_label, caller_locations[1]&.base_label 
    m0 == m1 and 
    (method(m0).owner rescue nil) == (method(m1).owner rescue nil) 
    end 
end 
+0

Le due chiamate 'method (mX) .owner' non dovrebbero essere valutate nel contesto corrente e essere sempre uguali? (assumendo 'm0 == m1') – ndn

+1

Sia' m0' che 'm1' sono stringhe e poiché sono uguali,' call_whatever_func_on_it (m0) 'sarebbe identicamente uguale a' call_whatever_func_on_it (m1) '. Mi manca smth? – mudasobwa

+0

Avevo paura di cosa succede quando c'è una chiamata ricorsiva. Ma non ho pensato molto. Forse posso semplificarlo con 'm0 == m1'. – sawa

3

Ecco un approccio più semplice (quasi banale), ma si deve passare sia, classe corrente e nome del metodo: (ho anche cambiato il nome metodo dal via_super? a called_via?)

module CallDetector 
    def called_via?(klass, sym) 
    klass == method(sym).owner 
    end 
end 

esempio di utilizzo:

class A 
    include CallDetector 

    def foo 
    called_via?(A, :foo) ? 'nothing special' : 'super!' 
    end 
end 

class B < A 
    def foo 
    super 
    end 
end 

class C < A 
end 

A.new.foo # => "nothing special" 
B.new.foo # => "super!" 
C.new.foo # => "nothing special"