Search code examples
rubygarbage-collectionfinalizer

Deleting an object in Ruby


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.


Solution

  • 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