How do I get the class of a BasicObject instance?

穿精又带淫゛_ 提交于 2019-12-17 17:41:31

问题


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 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.




回答3:


fguillen's link made me think of this way.

Pros:

  1. It doesn't need external libraries.

Cons:

  1. It must be executed before loading any classes that subclass BasicObject.
  2. 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 behavior 2.seconds.class remains Fixnum.
  • 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 the class 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

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!