Почему нельзя вызывать защищенные методы с символом proc?

Учитывая следующий класс:

class Foo
  def a
    dup.tap { |foo| foo.bar }
  end

  def b
    dup.tap(&:bar)
  end

  protected

  def bar
    puts 'bar'
  end
end

Кажется, что оба Foo#a и Foo#b должны быть эквивалентными, но это не так:

> Foo.new.a
bar
 => #<Foo:0x007fe64a951ab8>

> Foo.new.b
NoMethodError: protected method `bar' called for #<Foo:0x007fe64a940a88>

Есть ли причина для этого? Это ошибка?

Протестировано на Ruby 2.2.3p173

Ответ 1

Начнем с того, что в Ruby, как вам известно, в методе a, объявленном в классе Foo, я могу вызывать защищенные методы в любом экземпляре Foo.

Как Ruby определяет, есть ли у нас метод, объявленный в классе Foo? Чтобы понять это, нам придется выкопать внутренности вызова метода. Я буду использовать примеры из версии 2.2 МРТ, но, по-видимому, поведение и реализация одинаковы в других версиях (я бы хотел увидеть результаты тестирования этого на JRuby или Rubinious, хотя).

Ruby делает это в rb_call0. Как следует из комментария, self используется для определения того, можем ли мы назвать защищенные методы. self извлекается в rb_call из текущей информации кадра потока. Затем в rb_method_call_status мы проверяем, что это значение self имеет тот же класс, на котором определен защищенный метод.

Блоки путают проблему. Помните, что локальные переменные в Ruby-методе захватываются любым блоком, объявленным в этом методе. Это означает, что в блоке self является тем же самым self, на котором был вызван метод. Рассмотрим пример:
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

Запустив это, мы видим, что один и тот же экземпляр Foo одинаковый на всех уровнях, хотя мы назвали proc из совершенно другого объекта.

Итак, теперь мы понимаем, почему можно вызвать защищенный метод в другом экземпляре того же класса изнутри блока в методе.

Теперь посмотрим, почему proc, созданный с помощью &:bar, не может этого сделать. Когда мы помещаем знак & перед аргументом метода, мы делаем две вещи: проинструктируем ruby ​​передать этот аргумент как блок и поручить ему называть to_proc на нем.

Это означает вызов метода Symbol#to_proc. Этот метод реализован в C, но когда мы вызываем метод C, указатель на self на текущий кадр становится приемником этого метода C - в этом случае он становится символом :bar. Итак, мы смотрим на экземпляр Foo, который мы получили, как будто мы находимся в методе класса Symbol, и мы не можем вызывать защищенный метод.

Это глоток, но, надеюсь, он имеет достаточный смысл. Дайте мне знать, если у вас есть какие-либо предложения относительно того, как я могу улучшить его!