Reputation: 12181
Let's say I have the following class:
class Vehicle
@@total_vehicles = 0
@@all_instances = Array.new
def initialize
@@total_vehicles += 1
@@all_instances << self
end
def total_vehicles #returns total number of Vehicles 'alive'
return @@total_vehicles
end
def all_vehicles #returns an array of all Vehicle objects
return @@all_instances
end
end
Now to keep @@total_vehicles
and @@all_instances
up-to-date and correct, I want to make sure that they are correctly decremented and updated, respectively, when one of those objects is garbage collected. But here is what happens:
v = Vehicle.new
Vehicle.total_vehicles # => 1
v = nil #no references to Vehicle instance now
ObjectSpace.garbage_collect #instance garbage collected
Vehicle.total_vehicles # => 1 Nope!
Well I could add a finalizer Proc to each instance of the Vehicle class that, when called upon the object's garbage collection, would be called. But according to the documentation, ObjectSpace.define_finalizer(v,someProc)
would call someProc after the Vehicle instance is destroyed - meaning I cannot use self
or self.class
in there (since there would be no class, as there is no object!) I could have the proc call a public accessor method on the Vehicle class, but that takes away the purpose of class variables being accessible only to the class and its instances -> essentially turning the class variables into gvars.
How can I have the equivalent of a destructor method (from C++) that will get a Vehicle instance's affairs in order, as it were, before getting garbage-collected?
P.S.
ObjectSpace#count_objects
is no a viable option, as even the Ruby docs are up front about.
Upvotes: 1
Views: 12872
Reputation: 79562
Right now, they will never be garbage collected, as you are holding a reference in @@all_instances
. You could use a finalizer to get the result you want:
class Vehicle
class << self
attr_accessor :count
def finalize(id)
@count -= 1
end
def all #returns an array of all Vehicle objects
ObjectSpace.each_object(Vehicle).to_a
end
end
Vehicle.count ||= 0
def initialize
Vehicle.count += 1
ObjectSpace.define_finalizer(self, Vehicle.method(:finalize))
end
end
100.times{Vehicle.new}
p Vehicle.count # => 100
ObjectSpace.garbage_collect
p Vehicle.count # => 1, not sure why
p Vehicle.all # => [#<Vehicle:0x0000010208e730>]
If you run this code, you will see that it "works", except that there remains one Vehicle that is not garbage collected. I'm not sure why that is.
Your count
method could be also defined more simply by returning ObjectSpace.each_object(Vehicle).count
Finally, if you really want to maintain a list of existing Vehicles, you need to store their ID and use ObjectSpace._id2ref
:
require 'set'
class Vehicle
class << self
def finalize(id)
@ids.delete(id)
end
def register(obj)
@ids ||= Set.new
@ids << obj.object_id
ObjectSpace.define_finalizer(obj, method(:finalize))
end
def all #returns an array of all Vehicle objects
@ids.map{|id| ObjectSpace._id2ref(id)}
end
def count
@ids.size
end
end
def initialize
Vehicle.register(self)
end
end
Upvotes: 2
Reputation: 17192
What you almost certainly want here is the WeakRef class from the standard library. That handles all the details of object tracking and management without blocking reference counting.
Using a WeakRef that points to the object in your tracking you can delegate the whole finalization work to the library, and simplify your own life. (You may need to flush dead items from the arrays, but that is easily enough wrapped in your parent class.)
eg:
def all_instances
# this will vacuum out the dead references and return the remainder.
@@weakrefs_to_vehicles = @@weakrefs_to_vehicles.select(&:weakref_alive?)
end
def total_vehicles
all_instances.count
end
Upvotes: 3