如何获取BasicObject实例的类?

23
我有一个脚本,使用ObjectSpace#each_object迭代而不带参数。然后它会打印每个类存在多少实例。
我意识到一些类重新定义了#class实例方法,所以我必须找到另一种获取实际类的方法;假设它存储在变量"klass"中,并且klass === object为true。
在Ruby 1.8中,如果Object没有被猴子补丁修改,我可以这样做:
Object.instance_method(:class).bind(object).call

这对于ActiveSupport::Duration实例有效:
# 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

然而,在 Ruby 1.9 中,这种方法已经不再适用:

# 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>'

事实证明,ActiveSupport::Duration子类继承自ActiveSupport::BasicObject。后者在Ruby 1.9中被设计为继承自::BasicObject,因此Object被排除在继承链之外。这在Ruby 1.8中无法实现,因此ActiveSupport::BasicObject是Object的子类。
我没有找到任何一种方法来检测不是Object实例的Ruby 1.9对象的实际类。1.9中的BasicObject真的很简单:
BasicObject.instance_methods
=> [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__]

有什么想法吗?

更新:

由于Ruby 1.9已到达生命周期终点,我将接受@indirect的答案。上述提到的Ruby 1.9仅用于历史目的,以显示从1.8到1.9的更改是我问题的根本原因。


1
我曾经遇到过同样的问题,最终放弃了。有一些方法可供尝试(http://groups.google.com/group/comp.lang.ruby/browse_thread/thread/7e35b392a0b78768),但对我来说要么不起作用,要么太过侵入性。也许你可以重新定义你的问题,并直接指出你在原始代码中正在寻找的内容,而不是试图让其中一个可能的方法起作用。 - fguillen
@fguillen 感谢提供链接。关于使用self.inherited的文章看起来很有前途。 - Kelvin
接受了Frederick Cheung的答案。我选择它而不是我的解决方案,因为它可能表现更好。其他人可能有不同的要求或限制-只需投票支持您喜欢的答案即可。 - Kelvin
切换接受paon的答案。它不依赖于外部库,唯一的缺点是在您调用它的每个BasicObject上分配eigenclass。我唯一会做出的更改是将该方法定义为__realclass__而不是class - Kelvin
FYI:请看我的新答案,基于paon的解决方案。我保留了对paon的采纳,因为核心思想是他提出的。 - Kelvin
8个回答

13
下面的解决方案是针对特定类(eigenclass)的超类(superclass)。因此,它会产生副作用,即分配eigenclass(在MRI中可以通过ObjectSpace.count_objects [:T_CLASS]检测)。但由于BasicObject#class仅在空白状态对象上调用(即不是Object的子类,即不是Objects),因此该副作用也仅适用于空白状态对象。对于Objects,会调用标准的Kernel#class
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

编辑 - 注意: 实际上,ActiveSupport::Duration 存在问题。该类使用拦截 (method_missing) 重定向消息到 :value 属性。因此,它为其实例提供虚假的内省。为了保留这种错误性,需要为类映射使用另一个名称,例如建议的 __realclass__。因此,修改后的解决方案可能如下所示:

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

避免在Object上调用class << self的另一种方法是使用Module#===,正如Kelvin在此页面上建议的那样。


这简直太巧妙了... 它甚至可以在所有库都加载完成后才定义。但是你应该定义一个不同的方法名,比如 __realclass__,否则 20.seconds.class 将无法返回 Fixnum 类型 -- 这可能会破坏很多代码。如果你这样做,我就接受你的答案。 - Kelvin
顺便说一下,我尝试使用singleton_class.superclass,但是我得到了TypeError:无法定义singleton。猜测BasicObject就是这样奇怪。 - Kelvin
@Kelvin:Fixnum(以及其他立即值:falsetruenilSymbol)不是空白对象,因此像以前一样为它们调用Kernel#class。使用singleton_class无效,因为它是Kernel的实例方法。 - paon
@Kelvin:对于 ::Object === self 测试加一个赞(+1)。我不知道 Module#===Kernel#kind_of? 的反义词。 - paon
我刚意识到这为何可行。这是因为单例类是一个“Class”实例。ClassObject 的子类,这就是为什么 superclass 方法在实例上被定义的原因。当单例被创建时,“BasicObject”子类被设置为其超类。奇怪的 Ruby 魔法! - Kelvin

8
如果你可以升级到Ruby 2.0,那么你根本不需要实现任何东西:
>> Kernel.instance_method(:class).bind(BasicObject.new).call
=> BasicObject

1
有没有关于为什么这样做的信息链接?BasicObject不包括Kernel,那么为什么可以将Kernel方法绑定到它上面? - Kelvin
一个类似的技巧是:(class << BasicObject.new; self; end).superclass - rosenfeld
+1,纯Ruby的精华,不需要任何其他东西,而且这适用于从BasicObject派生的任何子类。虽然我同意@Kelvin的观点,但最好能够看到为什么它有效。 - MatzFan
2
回答@Kelvin的问题:看起来所有模块方法都可以以这种方式绑定到_任何_对象。MRI的源代码UnboundMethod#bind包括一个守卫子句,用于抛出错误,其开始为:if (!RB_TYPE_P(methclass, T_MODULE) &&...然后继续测试对象是否是未绑定方法的“所有者”类的kind_of? - 很好知道。 - MatzFan
感谢 @MatzFan。所以在 Ruby 2.x 中,bind 执行力度较弱。我想知道这个变化背后的理由是什么。 - Kelvin

5

我不知道如何使用Ruby来完成这个任务,但是使用C API对Ruby进行操作很简单。RubyInline Gem可以轻松地将C代码添加到你的Ruby代码中:

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

然后:

1.9.2p180 :033 > Example.true_class(20.minutes)
 => ActiveSupport::Duration 

这非常优雅,似乎不会干扰应用程序的其他部分。我已经点了赞,但在接受之前需要再考虑一下。 - Kelvin
顺便说一句,我认为在 FFI 中可能可以做类似的事情,但我没有找到将 Ruby 对象转换为 VALUE 指针和反之的方法。 - Kelvin
RubyInline似乎存在一个小错误 - 一旦创建了~/.ruby_inline目录,我就无法在irb中定义任何内联函数。Inline试图获取'(irb)'的修改时间。从外部文件加载它可以正常工作。 - Kelvin

5

fguillen的链接让我想到了这种方式。

优点:

  1. 它不需要外部库。

缺点:

  1. 必须在加载任何子类BasicObject的类之前执行。
  2. 它会向每个新类添加一个方法。

.

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__ }

当我阅读这个问题时,我也在考虑一个inherited钩子...很好的代码,我喜欢它! - Alex D

3
这是我对@paon答案的修改:
更改背后的原因:
- 方法名不会与现有库冲突,例如ActiveSupport::Duration实例行为2.seconds.class仍然是Fixnum。 - 由于Object没有自己的__realclass__方法,我们希望避免为这些实例分配特殊类。@paon的原始答案通过定义class方法名称来内在地执行此操作。
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

1
(class << object; self; end).superclass

是的,但这实际上与被接受的答案相同。 - Kelvin

0
在类似的情况下,你只需要创建一个从 BasicObject 继承的类,并支持 #class 方法,你可以将这个方法从 Kernel 复制过来。
class Foo < BasicObject
  define_method(:class, ::Kernel.instance_method(:class))
end

f = Foo.new
puts f.class
=> Foo

0

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://dev59.com/pWox5IYBdhLWcg3wcj94#10216927


这是一个不错的概念,但没有解决 ActiveSupport::Duration 问题,例如 2.seconds.class 应该返回 Fixnum。 - Kelvin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接