问题
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.
回答1:
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
回答2:
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 Object
s) the side effect also applies just for blank slate objects. For Object
s,
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 Object
s is via Module#===
, as suggested by Kelvin on this page.
回答3:
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__ }
回答4:
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
回答5:
This is my modification of @paon's answer:
Reasoning behind the changes:
- Method name doesn't clash with existing libs, e.g. the
ActiveSupport::Duration
instance behavior2.seconds.class
remainsFixnum
. - Since
Object
doesn'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 theclass
method 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
回答6:
(class << object; self; end).superclass
回答7:
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
回答8:
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