ruby - ¿Cómo obtengo la clase de una instancia de BasicObject?
ruby-1.9 (8)
Tengo un script que itera usando ObjectSpace#each_object
sin ObjectSpace#each_object
. Luego imprime cuántas instancias existen para cada clase.
Me di cuenta de que algunas clases redefinían el método de instancia #class
, por lo que tenía que encontrar otra forma de obtener la clase real; Digamos que está almacenado en la variable "klass"
, y el klass === object
es verdadero.
En Ruby 1.8, podría hacer esto, asumiendo que Object
no fue pareado:
Object.instance_method(:class).bind(object).call
Esto funcionó para las ActiveSupport::Duration
de 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
Pero, en Ruby 1.9 esto ya no funciona:
# 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>''
Resulta que las subclases ActiveSupport::BasicObject
ActiveSupport::Duration
ActiveSupport::BasicObject
. El último se hace a la subclase ::BasicObject
en Ruby 1.9, por lo que el Object
se excluye de la cadena de herencia. Esto no sucede y no puede suceder en Ruby 1.8, por lo que ActiveSupport::BasicObject
es una subclase de Object
.
No he encontrado ninguna forma de detectar la clase real de un objeto Ruby 1.9 que no sea una instancia de Object
. BasicObject
en 1.9 es realmente BasicObject
:
BasicObject.instance_methods
=> [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__]
Ideas?
ACTUALIZAR:
Desde que Ruby 1.9 llegó al final de su vida útil, estoy cambiando mi aceptación a la respuesta de @ indirect. Las menciones de ruby 1.9 anteriores son meramente con fines históricos, para mostrar que el cambio de 1.8 a 1.9 fue la causa original de mi problema.
El enlace de Fguillen me hizo pensar de esta manera.
Pros:
- No necesita librerías externas.
Contras:
- Debe ejecutarse antes de cargar cualquier clase que subclase BasicObject.
- Agrega un método a cada nueva clase.
.
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__ }
El siguiente código crea un módulo BasicKernel
mediante la duplicación del módulo Kernel
y la posterior eliminación de todos los métodos, excepto el método de class
. El BasicKernel
está incluido en la clase BasicObject
(al igual que Kernel
está incluido en Object
).
En req_methods
, puede especificar un subconjunto arbitrario de métodos de Kernel
para preservar.
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]
Edición: consulte la nota en https://.com/a/10216927/641718
Esta es mi modificación de la respuesta de @Paon:
Razonamiento detrás de los cambios:
- El nombre del método no choca con las bibliotecas existentes, por ejemplo, el comportamiento de la instancia de
ActiveSupport::Duration
2.seconds.class
sigue siendoFixnum
. - Como
Object
no tiene su propio método__realclass__
, queremos evitar la asignación de la eigenclass para esas instancias. La respuesta original de @ paon hizo esto inherentemente al definir el nombre del método declass
.
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
La siguiente solución se refiere a la superclase de eigenclass. Como consecuencia, tiene el efecto secundario de asignar la eigenclass (detectable por ObjectSpace.count_objects[:T_CLASS]
en MRI). Pero como la BasicObject#class
solo se invoca en objetos de pizarra en blanco (es decir, los objetos que no son objetos de tipo, es decir, que no son Object
), el efecto secundario también se aplica solo a los objetos de pizarra en blanco. Para los Object
s , se invoca la Kernel#class
estándar de 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
Editar - Nota: De hecho, hay un problema con ActiveSupport::Duration
. Esta clase usa intercepción ( method_missing
) para redirigir mensajes al atributo :value
. Como consecuencia, proporciona falsa introspección para sus instancias. Para preservar esta falsedad, es necesario usar otro nombre para el mapa de clase, por ejemplo, el __realclass__
propuesto. Por lo tanto, la solución modificada podría tener este aspecto:
class BasicObject
def __realclass__; (class << self; self end).superclass end
end
class Object; alias __realclass__ class end
Otra forma de no invocar la class << self
en Object
s es a través del Module#===
, como sugiere Kelvin en esta página.
No sé cómo hacerlo en Ruby, pero es sencillo usar la API de C para Ruby. La gema RubyInline hace que agregar bits de C a tu código Ruby sea bastante fácil:
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
Y entonces:
1.9.2p180 :033 > Example.true_class(20.minutes)
=> ActiveSupport::Duration
Para la situación similar en la que simplemente desea que una clase que creó que hereda de BasicObject
compatible con el método #class
, puede copiar el método desde Kernel
.
class Foo < BasicObject
define_method(:class, ::Kernel.instance_method(:class))
end
f = Foo.new
puts f.class
=> Foo
Si puedes actualizar a Ruby 2.0, no necesitas implementar nada:
>> Kernel.instance_method(:class).bind(BasicObject.new).call
=> BasicObject
(class << object; self; end).superclass