Reputation: 2057
Within PHP classes the parser deals with the __construct
and __destruct
methods to instantiate the instance and destroy it when the script exits or you use unset. When you extend a class you simply use parent::__construct
and parent::__destruct
to run any cleanup code that might need running on the class that was extended.
Now within the context of a class that represents DB data and helps you manipulate that data I'd thought that a __destruct
method could be used to compare current values against the originals grabbed from the DB and do an UPDATE when necessary (in some cases just always do an UPDATE so long as the Primary Key value cannot be changed). Implementing this in PHP is pretty straight forward.
The main upside to this approach would be to simply manipulate class variables quickly as needed and then have the class do one big UPDATE at the end. In long scripts that run for minutes it might be nice to create DB instance during __construct, get the data, close the DB connection, and then manipulate class variables only during the minutes long execution. On __destruct, open up a new DB connection make the UPDATEs and then close down the DB connection and clean up anything else that needs cleaning up.
I'm curious what people's thoughts are on whether or not this is a good idea/bad practice but my main question was is this possible in Ruby.
In Ruby you have the initialize method that runs when you instantiate an instance of the class. The Ruby equivalent of parent::__construct
is super
in Ruby. And there is the ObjectSpace.define_finalize
and finalize
method for Ruby classes. However, as I understand it, the finalize method isn't supposed to be able to reference the instance calling it. On top of that I can't find any equivalent to parent::__destruct
. I suspect that's because there isn't an equivalent since it seems that the finalize
method was explicitly designed to prevent referencing itself.
Anyone out there know of way to do this? If not, what's the best practice for dumping Ruby classes to get back resources and prevent data loss? Does everyone have a garbage_collection method they call just before setting a class instance to nil or is there some other way?
Thanks
Upvotes: 5
Views: 6369
Reputation: 1703
No, Ruby has no equivalent to PHP __destruct
, because PHP destroys an object as soon as its reference count reaches zero, but Ruby is slow to destroy objects. Ruby marks and sweeps objects only from time to time. Also, Ruby is conservative when it scans for local variables of C code. Ruby refuses to destroy an object if a C local might point to it.
Ruby has ObjectSpace.define_finalizer
but it is difficult to use. An object can have more than one finalizer. If the superclass defined a finalizer, you can define another finalizer on the same object. The problem is that ObjectSpace.undefine_finalizer
removes all finalizers from the object. So if the superclass removes its finalizer, it also removes your finalizer.
Ruby runs the finalizer only after destroying the object. The finalizer must not have a reference to the object. If it does, Ruby never destroys the object, and you have a memory leak. The finalizer must not be in a scope where self
or a local variable refers to the object.
In the next example, I show how to use a finalizer to update a database. This is a bad idea because Ruby is slow to run finalizers. The database might not get updated for a long time. I call GC.start
to force a full garbage collection. My computer's Ruby refuses to destroy one of the objects, so one update doesn't happen yet.
require 'forwardable'
require 'pstore'
# Create a database of people's ages.
People = PStore.new('people.pstore')
People.transaction do
People['Alice'] = 20
People['Bob'] = 30
People['Carl'] = 40
People['David'] = 50
People['Eve'] = 60
end
# Shows people in database. This can show old values if someone
# forgot to update the database!
def show_people(heading)
People.transaction(true) do
puts heading
%w[Alice Bob Carl David Eve].each do |name|
puts " #{name} is #{People[name]} years old."
end
end
end
show_people("Before birthdays:")
# This is a person in the database. You can change his or her age,
# but the database is only updated when you call #update or by this
# object's finalizer.
class Person
# We need a PersonStruct for the finalizer, because Ruby destroys
# the Person before calling the finalizer.
PersonStruct = Struct.new(:name, :age, :oage)
class PersonStruct
def update(_)
s = self
if s.age != s.oage
People.transaction { People[s.name] = s.oage = s.age }
end
end
end
private_constant :PersonStruct
# Delegate name (r) and age (rw) to the PersonStruct.
extend Forwardable
def_delegators(:@struct, :name, :age, :age=)
# Find a person in the database.
def initialize(name)
age = People.transaction(true) { People[name] }
@struct = PersonStruct.new(name, age, age)
ObjectSpace.define_finalizer(self, @struct.method(:update))
end
# Update this person in the database.
def update
@struct.update(nil)
end
end
# Now give everyone some birthdays.
Person.new('Alice').age += 2
Person.new('Bob').age += 1
Person.new('Carl').age += 1
Person.new('David').age += 1
Person.new('Eve').age += 2
# I forgot to keep references to the Person objects and call
# Person#update. Now I try to run the finalizers.
GC.start
# Did the database get updated?
show_people("After birthdays:")
My computer gives this output:
Before birthdays:
Alice is 20 years old.
Bob is 30 years old.
Carl is 40 years old.
David is 50 years old.
Eve is 60 years old.
After birthdays:
Alice is 22 years old.
Bob is 31 years old.
Carl is 41 years old.
David is 51 years old.
Eve is 60 years old.
I added 2 years to Eve's age, but there was no update before I checked the database. The C code that interprets Ruby might have left a reference to Person.new('Eve')
in some local variable, so Ruby would not destroy the object. The result might change if you use another version of Ruby or a different C compiler. Ruby does run any leftover finalizers when the program exits, so the update did happen, but it was too late.
Upvotes: 6
Reputation: 3202
as pst noted in his comment you don't need a destructor for ruby. Just set all referring variables to null ( ref = nil ) and the object will get deleted by garbage collection. You can not know exactly when its garbace collected (deleted). In addition you could ( not that I recommend ) write a proc that runs before actuall deletion of that object
ObjectSpace.define_finalizer(self, self.class.method(:finalize).to_proc)
Upvotes: 4