Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I get the class of a BasicObject instance?

Tags:

I have a script that iterates using ObjectSpace#each_object with no args. Then it prints how many instances exist for each class.

I realized that some classes redefine the #class instance method, so I had to find another way to get the actual class; Let's say it's stored in variable "klass", and klass === object is true.

In Ruby 1.8 I could do this, assuming Object wasn't monkeypatched:

Object.instance_method(:class).bind(object).call 

This worked for ActiveSupport::Duration instances:

# Ruby 1.8 # (tries to trick us) 20.seconds.class => Fixnum # don't try to trick us, we can tell Object.instance_method(:class).bind(20.seconds).call => ActiveSupport::Duration 

But, in Ruby 1.9 this no longer works:

# Ruby 1.9 # we are not smart... Object.instance_method(:class).bind(20.seconds).call TypeError: bind argument must be an instance of Object   from (irb):53:in `bind'   from (irb):53   from /Users/user/.rvm/rubies/ruby-1.9.2-p0/bin/irb:17:in `<main>' 

It turns out that ActiveSupport::Duration subclasses ActiveSupport::BasicObject. The latter is made to subclass ::BasicObject in Ruby 1.9, so Object is excluded from the inheritance chain. This doesn't, and can't, happen in Ruby 1.8, so ActiveSupport::BasicObject is a subclass of Object.

I haven't found any way to detect the actual class of a Ruby 1.9 object that isn't an instance of Object. BasicObject in 1.9 is really bare-bones:

BasicObject.instance_methods => [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__] 

Ideas?

UPDATE:

Since ruby 1.9 reached end-of-life, I'm changing my accept to @indirect's answer. The mentions of ruby 1.9 above are merely for historical purposes, to show that the change from 1.8 to 1.9 was the original cause of my problem.

like image 334
Kelvin Avatar asked Feb 08 '12 16:02

Kelvin


1 Answers

The following solution refers to the superclass of the eigenclass. As a consequence, it has the side effect of allocating the eigenclass (detectable by ObjectSpace.count_objects[:T_CLASS] in MRI). But since BasicObject#class is only invoked on blank slate objects (i.e. objects that are not kind-of Object, i.e. that are not Objects) the side effect also applies just for blank slate objects. For Objects, the standard Kernel#class is invoked.

class BasicObject   def class     (class << self; self end).superclass   end end  # tests: puts RUBY_VERSION               # 1.9.2 class B < BasicObject; end class X;               end p BasicObject.new.class             # BasicObject p B          .new.class             # B p X          .new.class             # X p               6.class             # Fixnum p B.instance_method(:class).owner   # BasicObject p X.instance_method(:class).owner   # Kernel p          6.method(:class).owner   # Kernel 

Edit - Note: Indeed, there is an issue with ActiveSupport::Duration. This class uses interception (method_missing) for redirecting messages to the :value attribute. As a consequence, it provides false introspection for its instances. To preserve this falsity, it is necessary to use another name for the class map, e.g. the proposed __realclass__. Thus, the modified solution might look like this:

class BasicObject   def __realclass__; (class << self; self end).superclass end end class Object; alias __realclass__ class end 

Another way of not invoking class << self on Objects is via Module#===, as suggested by Kelvin on this page.

like image 169
paon Avatar answered Oct 26 '22 06:10

paon