Given the following class:
class Foo
def a
dup.tap { |foo| foo.bar }
end
def b
dup.tap(&:bar)
end
protected
def bar
puts 'bar'
end
end
It seems like both Foo#a
and Foo#b
should be equivalent, but they're not:
> Foo.new.a
bar
=> #<Foo:0x007fe64a951ab8>
> Foo.new.b
NoMethodError: protected method `bar' called for #<Foo:0x007fe64a940a88>
Is there a reason for this? Is this a bug?
Tested on Ruby 2.2.3p173
Let's start by noting that in Ruby, as you probably know, in a method a
declared on a class Foo
, I can invoke protected methods on any instance of Foo
.
How does Ruby determine whether we're in a method declared on class Foo
though? To understand this, we'll have to dig into the internals of method invocation. I'll be using examples from version 2.2 of MRI, but presumably the behavior and implementation are the same in other versions (I'd love to see results from testing this on JRuby or Rubinious, though).
Ruby does this in rb_call0
. As the comment suggests, self
is used to determine whether we can call protected methods. self
is retrieved in rb_call
from the current thread's call frame info. Then, in rb_method_call_status
, we check that this value of self
is of the same class on which a protected method is defined.
Blocks confuse the issue somewhat. Remember that local variables in a Ruby method are captured by any block declared in that method. This means that in the block, self
is the same self
on which the method was invoked. Let's look at an example:
class Foo
def give_me_a_block!
puts "making a block, self is #{self}"
Proc.new do
puts "self in block0 is #{self}"
end
end
end
proc = Foo.new.give_me_a_block!
proc.call
Running this, we see the same instance of Foo
is the same at all levels, even though we called the proc from a completely different object.
So, now we understand why it's possible to call a protected method on another instance of the same class from within a block in a method.
Now let's look at why a proc created with &:bar
can't do this. When we place an &
sign before a method argument we do two things: instruct ruby to pass this argument as a block, and instruct it to call to_proc
on it.
This means invoking the method Symbol#to_proc
. This method is implemented in C, but when we invoke a C method, the pointer to self
on the current frame becomes the receiver of that C method -- in this case, it becomes the symbol :bar
. So we're looking at the instance of foo
we got as though we are in a method of class Symbol, and we can't invoke a protected method.
That's a mouthful, but hopefully it makes enough sense. Let me know if you have any suggestions as to how I might improve it!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With