当前的 Ruby 方法是通过 super 调用的吗?

Is the current Ruby method called via super?

在运行时的方法中,有没有办法知道该方法是否已在子类中通过 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!"

我怎么写via_super?,或者,如果需要,via_super?(:bar)

可能有更好的方法,但总体思路是 Object#instance_of? 仅限于当前 class,而不是层次结构:

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!"


但是,请注意,这不需要在 child 中显式 super。如果 child 没有这样的方法而使用 parent 的方法,via_super? 仍然会 return true。我认为除了检查堆栈跟踪或代码本身之外,没有办法只捕获 super 情况。

出色的@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!"

这里我们使用Kernel#caller来确保调用的方法的名称与super中的名称匹配class。如果不是直接后代,这种方法可能需要一些额外的调整(caller(2) 应该更改为更复杂的分析,)但你可能明白了。

UPD 感谢@Stefan 对其他答案的评论,更新为 unless defined 以使其在 FooFu include SuperDetector.

UPD2 使用祖先来检查超级而不是直接比较。

编辑 根据 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

这是一个更简单(几乎微不足道)的方法,但您必须同时传递当前 class 和方法名称:(我还将方法名称从 via_super? 更改为 called_via?)

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

用法示例:

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"

, and 答案与递归支持的终极组合:

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

唯一不起作用的情况 (AFAIK) 是如果你在基础中有间接递归 class,但除了解析代码之外我不知道如何处理它。