Access class if class method is overwritten

Viewed 115

Here is an external class which has its class method overwritten.

class Foo
  def class
    "fooo"
  end
end

class Boo < Foo
end

class Moo < Foo
end

Now I have an instance of the subclass. Is it possible to find out which class it belongs to?

foo.class # currently returns 'fooo', I want get Boo or Moo.
4 Answers

You could use instance_method to grab the class method from somewhere safe (such as Object) as an UnboundMethod, bind that unbound method to your instance, and then call it. For example:

class_method = Object.instance_method(:class)
# #<UnboundMethod: Object(Kernel)#class> 

class_method.bind(Boo.new).call
# Boo 

class_method.bind(Moo.new).call
# Moo 

class_method.bind(Foo.new).call
# Foo 

Of course, if you've also replaced Object#class (or Kernel#class) then all bets are off and you're in a whole new world of pain and confusion.

I prefer @Stefan’s and @muistooshort's solutions, but here's an alternative.

class Foo
  def class
    "foo"
  end
end

class Boo < Foo
end

class Who < Boo
  def class
    "who"
  end
end

boo = Boo.new
boo.method(:class).super_method.call
  #=> Boo

who = Who.new
who.method(:class).super_method.call
  #=> "foo"
who.method(:class).super_method.super_method.call
  #=> Who

More generally:

def my_class(obj)
  m = obj.method(:class)
  until m.owner == Kernel do
    m = m.super_method
  end
  m.call
end

my_class(boo)
  #=> Boo 
my_class(who)
  #=> Who

See Method#super_method.

An object's class also happens to be the superclass of its singleton_class:

Boo.new.singleton_class.superclass
#=> Boo

Moo.new.singleton_class.superclass
#=> Moo

Foo.new.singleton_class.superclass
#=> Boo

This solution is a little hacky, but overriding class is pretty hacky in itself, so when in Rome, do as Romans do:

class Foo
  def class
    'fooo'
  end
end

class Boo < Foo
end

boo = Boo.new
=> #<Boo:0x00007fd2361feba8>

boo.class
=> "fooo"

boo.inspect
=> "#<Boo:0x00007fd2361feba8>"

klass = boo.inspect.split(':').reject(&:empty?)[0..-2].join('::').sub('#<', '')
=> "Boo"

boo.is_a?(Kernel.const_get(klass))
=> true

This also works for classes in a module:

module Bar
  class Foo
    def class
      'fooo'
    end
  end

  class Boo < Foo
  end
end

boo = Bar::Boo.new
=> #<Bar::Boo:0x00007fe5a20358b0>

boo.class
=> "fooo"

boo.inspect
=> "#<Bar::Boo:0x00007fe5a20358b0>"

klass = boo.inspect.split(':').reject(&:empty?)[0..-2].join('::').sub('#<', '')
=> "Bar::Boo"

boo.is_a?(Kernel.const_get(klass))
=> true
Related