I've read stackoverflow posts on this topic as well as several articles which include A Primer on Ruby Method Lookup, What is the method lookup path in Ruby. In addition, I checked out the object model chapter in Ruby Metaprogramming 2, asked in a few chat rooms, and made this reddit thread. Short of learning C, I've done what I can to figure this out.
As described by the resources above, these 6 places are checked (in order) during method lookup on a receiving object like fido_instance:
- singleton class of fido_instance
- IClass (from an extended module)
- IClass (from a prepended module)
- class
- IClass (from an included module)
- superclass (if method isn't found here, repeat steps 4-6)
Obviously, the diagram is incomplete, and all of these singleton classes might not have been created in the real world. Still, those 6 steps leave a lot to be desired, and don't cover the following scenario. If there were no extended/prepended IClass above the singleton class of fido_instance
, then there's no explanation of whether step 4 is executed on the singleton class of fido_instance
. I have to assume not since the whole method lookup would short circuit.
If I were to guess a set of steps that could explain ruby's method lookup behavior, it might look like:
- check
fido_instance.class
for the method. (obviously, ruby isn't going to use its own #class method to do the method lookup, but it conveys the logic of the process) - check
fido_instance.class.superclass
for the method. Keep adding.superclass
and checking for the method until no superclasses are left. (again, ruby isn't going to use its own #superclass method) - method wasn't found. Start at step 1, looking for #method_missing this time.
I also recall reading that there's a separate method lookup process if the receiving object is a class, but I can't recall where.
So what's the correct, detailed explanation that doesn't involve knowing C?
There's a ... gem ... in that second ref that I think gets to the core of the answer: ancestors of the singleton class. Applied to your object, it would be:
fido_instance.singleton_class.ancestors
This will always give you the order of method lookup that Ruby uses. It's pretty simple when you view it this way, and that's the bottom line answer to your question. Ruby will start at the singleton_class and work its way up the ancestors looking for that method. Using your diagram:
fido.singleton_class.ancestors
=> [Fetch, WagTail, DogClass, Object, Kernel, BasicObject]
(Note1: Bark
is not part of this output because you used extend
instead of include
. More on this in a second.)
(Note2: If it doesn't find it all the way up to BasicObject
, then it will call method_missing
up the same ancestry chain.)
It's no different when calling a method on a class, because in Ruby a class it just an instance of class Class
. So DogClass.method1
will search for method1
on DogClass.singleton_class
and then up its ancestry chain, just like before.
DogClass.singleton_class.ancestors
=> [Bark, Class, Module, Object, Kernel, BasicObject]
Since you used extend
for Bark
, this is where we find it! So if Bark
defined a method bark
, then you can call DogClass.bark
because that method is defined in DogClass
's singleton_class' ancestors.
To understand what that ancestry tree will be (instead of relying on printing it out every time), you simply need to know how the ancestry is modified by subclassing, extend
, include
, prepend
, etc.
- Subclassing gives the child class the entire ancestry chain of its superclass.
include
ing a module in a classC
adds that module into the ancestry chain afterC
and before everything else.prepend
ing a module in a classC
adds that module into the ancestry chain before everything, includingC
and any currently prepended modules.def x.method1
addsmethod1
tox.singleton_class
. Similarlyx.extend(M)
will addM
to the ancestry ofx.singleton_class
(but not tox.class
). Note that the latter is exactly what happened withBark
andDogClass.singleton_class
, but can equally apply to any object.
Leaving out extend
from the above list because it does not modify the object's ancestry chain. It does modify the ancestry of that object's singleton_class
-- as we saw, Bark
was included in DogClass.singleton_class.ancestors
.
Tangent:
The bit about class methods above is the key to me for understanding how important singleton classes are to Ruby. You obviously can't define bark
on DogClass.class
, because DogClass.class == Class
and we don't want bark
on Class
! So how can we allow DogClass
to be an instance of Class
, allowing it to have a (class) method bark
that is defined for DogClass
but not unrelated classes? Using the singleton class! In this way, defining a "class method", like by def self.x
inside class C
, is sort of like C.singleton_class.send(:define_method, :x) {...}
.
来源:https://stackoverflow.com/questions/50286933/what-are-the-actual-steps-in-rubys-method-lookup