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.
If you can upgrade to Ruby 2.0, you don't need to implement anything at all:
>> Kernel.instance_method(:class).bind(BasicObject.new).call
=> BasicObject
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.
fguillen's link made me think of this way.
Pros:
- It doesn't need external libraries.
Cons:
- It must be executed before loading any classes that subclass BasicObject.
- It adds a method to every new class
.
class BasicObject
def self.inherited(klass)
klass.send(:define_method, :__realclass__) { klass }
end
def __realclass__
BasicObject
end
end
# ensures that every Object will also have this method
class Object
def __realclass__
Object.instance_method(:class).bind(self).call
end
end
require 'active_support/core_ext'
20.seconds.__realclass__ # => ActiveSupport::Duration
# this doesn't raise errors, so it looks like all objects respond to our method
ObjectSpace.each_object{|e| e.__realclass__ }
I don't know about doing it in Ruby, but this is straightforward using the C API to Ruby. The RubyInline Gem makes adding bits of C to your Ruby code quite easy:
require 'inline'
class Example
inline do |builder|
builder.c_raw_singleton <<SRC, :arity => 1
VALUE true_class(VALUE self, VALUE to_test) {
return rb_obj_class(to_test);
}
SRC
end
end
And then:
1.9.2p180 :033 > Example.true_class(20.minutes)
=> ActiveSupport::Duration
This is my modification of @paon's answer:
Reasoning behind the changes:
- Method name doesn't clash with existing libs, e.g. the
ActiveSupport::Durationinstance behavior2.seconds.classremainsFixnum. - Since
Objectdoesn't have its own__realclass__method, we want to avoid allocating the eigenclass for those instances. @paon's original answer did this inherently by defining theclassmethod name.
class BasicObject
def __realclass__
::Object === self ?
# Note: to be paranoid about Object instances, we could
# use Object.instance_method(:class).bind(s).call.
self.class :
(class << self; self end).superclass
end
end
# test
require 'active_support/core_ext/integer'
require 'active_support/core_ext/numeric'
duration = 2.seconds
string = 'hello world'
p duration.class # => Fixnum
p string.class # => String
GC.start
p ObjectSpace.count_objects[:T_CLASS] # => 566
# creates the eigenclass
p duration.__realclass__ # => ActiveSupport::Duration
p ObjectSpace.count_objects[:T_CLASS] # => 567
# doesn't create the eigenclass
p string.__realclass__ # => String
p ObjectSpace.count_objects[:T_CLASS] # => 567
(class << object; self; end).superclass
The following code creates a BasicKernel module via duplication of the Kernel module and subsequent removal of all methods except the class method. The BasicKernel is included into the BasicObject class (just like Kernel is included into Object).
In req_methods, you can specify arbitrary subset of Kernel methods to be preserved.
class BasicObject
include ::BasicKernel = ::Kernel.dup.module_eval {
v = $VERBOSE
$VERBOSE = nil # suppress object_id warning
req_methods = [:class] # required methods (to be preserved)
all_methods = public_instance_methods +
protected_instance_methods +
private_instance_methods
all_methods.each { |x| remove_method(x) unless req_methods.include?(x) }
$VERBOSE = v
self
}
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 B.instance_method(:class).owner # BasicKernel
p X.instance_method(:class).owner # Kernel
p Object.ancestors # [Object, Kernel, BasicObject, BasicKernel]
p BasicKernel.instance_methods # [:class]
Edit: See the Note in https://stackoverflow.com/a/10216927/641718
For the similar situation where you simply want a class you created that inherits from BasicObject to support the #class method, you can copy the method over from Kernel.
class Foo < BasicObject
define_method(:class, ::Kernel.instance_method(:class))
end
f = Foo.new
puts f.class
=> Foo
来源:https://stackoverflow.com/questions/9197586/how-do-i-get-the-class-of-a-basicobject-instance