可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
#inherited
is called right after the class Foo
statement. I want something that'll run only after the end
statement that closes the class declaration.
Here's some code to exemplify what I need:
class Class
def inherited m
puts "In #inherited for #{m}"
end
end
class Foo
puts "In Foo"
end
puts "I really wanted to have #inherited tiggered here."
### Output:
# In #inherited for Foo
# In Foo
# I really wanted to have #inherited tiggered here.
Does anything like that exist? Can it be created? Am I totally out of luck?
回答1:
I am late, but I think I have an answer (to anyone who visit here).
You can trace until you find the end of the class definition. I did it in a method which I called after_inherited
:
class Class
def after_inherited child = nil, &blk
line_class = nil
set_trace_func(lambda do |event, file, line, id, binding, classname|
unless line_class
# save the line of the inherited class entry
line_class = line if event == 'class'
else
# check the end of inherited class
if line == line_class && event == 'end'
# if so, turn off the trace and call the block
set_trace_func nil
blk.call child
end
end
end)
end
end
# testing...
class A
def self.inherited(child)
after_inherited do
puts "XXX"
end
end
end
class B < A
puts "YYY"
# .... code here can include class << self, etc.
end
Output:
YYY
XXX
回答2:
You may be out of luck. But that's only a warning, not a definitive answer.
Ruby hooks the beginning of the class definition, rather than the end, for Class#inherited
b/c ruby class definitions don't have a real end. They can
be reopened any time.
There was some talk a couple years ago about adding a const_added trigger, but it hasn't gone through yet. From Matz:
I'm not going to implement every possible hook. So when somebody
comes with more concrete usage, I will consider this again. It would
be const_added
, not class_added
.
So this might handle your case - but I'm not sure (it may trigger on the start too, when it's eventually implemented).
What are you trying to do with this trigger? There may be another way to do it.
回答3:
Use TracePoint
to track when your class sends up an :end
event.
This module will let you create a self.finalize
callback in any class.
module Finalize
def self.extended(obj)
TracePoint.trace(:end) do |t|
if obj == t.self
obj.finalize
t.disable
end
end
end
end
Now you can extend your class and define self.finalize
, which will run as soon as the class definition ends:
class Foo
puts "Top of class"
extend Finalize
def self.finalize
puts "Finalizing #{self}"
end
puts "Bottom of class"
end
puts "Outside class"
# output:
# Top of class
# Bottom of class
# Finalizing Foo
# Outside class
回答4:
Take a look at defined
gem. You can do like this:
require "defined"
Defined.enable!
class A
def self.after_inherited(child)
puts "A was inherited by #{child}"
end
def self.defined(*args)
superclass.after_inherited(self) if superclass.respond_to?(:after_inherited)
end
end
class B < A
puts "B was defined"
end
Output:
B was defined
A was inherited by B
However self.defined
will be fired after each class definition. So if you add the following code
class B < A
puts "B was redefined"
end
You will see
B was defined
A was inherited by B
B was redefined
A was inherited by B
There are ways to avoid that which I can explain to you if you want.
However, as said there are probably better ways to solve your problem.
回答5:
If you are willing to assume your Ruby implements ObjectSpaces, you could could look up all model instances after the fact, and then modify them appropriately. Google suggests http://phrogz.net/ProgrammingRuby/ospace.html
回答6:
Rails has a subclasses
method, it might be worth looking at the implementation:
class Fruit; end
class Lemon < Fruit; end
Fruit.subclasses # => [Lemon]
回答7:
I had the same question when trying to automatically add generic validations to all models. The problem was that if the model used #set_table_name then my code that added added the validations based on the DB data-types would blow up because it was guessing at the table name based on the name of the model (because #inherited was getting called BEFORE #set_table_name).
So just like you, I was really looking for a way to get #inherited to fire AFTER everything in the model had already been loaded. But I didn't really need to take it that far, all I needed was something that fired AFTER #set_table_name. So it turned out to be a simple as aliasing the method. You can see an example of what I did here:
https://gist.github.com/1019294
In a comment above you stated "I'm trying to add behavior to activerecord models, but I need all the model customizations to go through before I mess with it". So my question to you is if there are specific model customizations you care about, if so then maybe you could use an aliasing approach to achieve your desired result.
回答8:
No, there no such a hook to my knowledge, but the good thing is you can kind of do it yourself. Here is a possible implementation:
Is not super clean, but it works:
puts RUBY_VERSION # 2.4.1
class Father
def self.engage_super_setup(sub)
puts "self:#{self} sub:#{sub}"
sub.class_eval do
puts "toy:#{@toy}"
end
end
def self.super_setup
if self.superclass.singleton_methods.include?(:engage_super_setup)
superclass.engage_super_setup(self)
end
end
end
Son = Class.new(Father) do
@toy = 'ball'
end.tap { |new_class| new_class.super_setup } # this is needed to:
# 1. call the super_setup method in the new class.
# 2. we use tap to return the new Class, so this class is assigned to the Son constant.
puts Son.name # Son
Output:
self:Father sub:#<Class:0x0055d5ab44c038> #here the subclass is still anonymous since it was not yet assigned to the constant "Son"
toy:ball # here we can see we have acess to the @toy instance variable in Son but from the :engage_super_setup in the Father class
Son # the of the class has been assigned after the constant, since ruby does this automatically when a class is assigned to a constant
So this is obviously not as clean as a hook, but I think at the end we have a pretty good result.
If we had tried to do the same with :inherited sadly is not possible, because :inherited is called even before the execution entoer in the body of the class:
puts RUBY_VERSION # 2.4.1
class Father
def self.inherited(sub)
puts "self:#{self} sub:#{sub}"
sub.class_eval do
puts "toy:#{@toy.inspect}"
end
end
end
class Son < Father
puts "we are in the body of Son"
@toy = 'ball'
end
puts Son.name # Son
Output:
self:Father sub:Son # as you can see here the hook is executed before the body of the declaration Son class runs
toy:nil # we dont have access yet to the instance variables
we are in the body of Son # the body of the class declaration begins to run after the :inherited hook.
Son