Object oriented progamming : advanced concepts
By Samir Tine, published on January 2023
Destructors
You probably remember the constructor concept, which allows you to initialize an instance with specific attributes/methods/properties. The concept of destructor is the same, but relates to when the instance is destroyed in memory :
-- Defines an INIFile object that inherits from sys.File
INIFile = Object(sys.File)
-- Adds a constructor
function INIFile:constructor(filename)
-- Adds an instance field "file"
self.file = sys.File(filename)
-- Checks that its an ini file
if self.file.extension ~= ".ini" then
error("INIFile constructor expects a INI file")
end
-- Opens the ini file for reading
self.file:open()
print("INIFile instance constructed")
end
-- Adds a destructor
function INIFile:destructor()
print("INIFile instance destructed")
self.file:close()
end
-- Creates a new INIFile instance
local config = INIFile("config.ini")
Once the program is finished, the garbage collector will do the necessary to erase all objects created in memory and you will see the message "INIFIle object destroyed".
Using a destructor is simple as adding a destructor()
method when defining an object. Destructors are very handy for finalizing/freeing up resources once the object is no longer in use. But be careful, we don't control when the destructor will be called by the garbage collector.
-- Creates a new INIFile instance
local config = INIFile("config.ini")
-- Read the entire file content using the File:read() method (inherited)
local content = config:read()
-- set config to nil to destruct the INIFile instance
config = nil
print("End of program")
-- The destructor is not called before the "end of the program" message !
Setting to nil
and instance or going out from scope (using a local instance), do not call the instance destructor method immediately. Because Luart uses a garbage collector, the destructor is called only when a new garbage collection cycle is started.
You can however force garbage collection if needed, to ensure that a destructor is immediately called upon instance destruction :
-- Creates a new INIFile instance
local config = INIFile("config.ini")
-- Read the entire file content using the File:read() method (inherited)
local content = config:read()
-- set config to nil to destruct the INIFile instance
config = nil
-- forces a garbage colleciton cycle..
collectgarbage()
-- ..that will call this time the INIFile destructor before "End of program" message
print("End of program")
Iterators
The iterator construct allows you to traverse elements of a container or collection with for..in..do
instructions. In Lua, these collections are often tables, which are used to construct arrays and other data structures. Collections can also be represented by objects in Luart. An iterator can iterate over an instance that represents a collection. Our object definition only needs to include a new behaviour/method to use such functionality :
-- Defines a BookStore object
BookStore = Object {}
-- Adds a constructor
function BookStore:constructor(books)
-- Adds an iterator function
function self.iterator(index)
-- index is set to nil when the iteration begins
index = index or 1
-- the new index value should be returned as the last value
return books[index], index+1
end
end
-- Creates a new BookStore instance
local bookstore = BookStore { "Romeo and Juliet", "Of mice and men", "The Wizard of Oz" }
-- prints the books in the bookstore using a for loop
for i, book in bookstore do
print("Title : "..i.." "..book)
end
This example illustrates a BookStore instance that defines an iterator using the provided books table in the instance constructor. An iterator method has a single parameter that represent the current indexed value in the loop. During the first iteration, this parameter is set to nil
.
Once the value associated with the requested index has been calculated, that value is returned, followed by the next index (which will be used during the next iteration/call of the iterator method). The iteration stops when the nil
value is returned.
Note that an iterator method can return more than one value :
function BookStore:constructor(books)
-- Adds an iterator function
function self.iterator(index)local Iterable = {
items = {},
iterator = function (self, index)
-- index is set to nil when the iteration begins
-- the new index value should be returned as the last value
local index, value = next(self.items, index)
return index, value, index
end
}
-- Defines a BookStore object
BookStore = Object({}, Iterable)
-- Adds a constructor
function BookStore:constructor(books)
self.items = books
end
-- Creates a new BookStore instance
local bookstore = BookStore { "Romeo and Juliet", "Of mice and men", "The Wizard of Oz" }
-- prints the books in the bookstore using a for loop
for i, book in bookstore do
print("Book #"..i..": "..book)
end
-- index is set to nil when the iteration begins
index = index or 1
-- the new index value should be returned as the last value
-- if the value is not nil, return the index and the associated value
local value = books[index]
return value and index or nil, value, index+1
end
end
-- Creates a new BookStore instance
local bookstore = BookStore { "Romeo and Juliet", "Of mice and men", "The Wizard of Oz" }
-- prints the index and the books in the bookstore using a for loop
for i, book in bookstore do
print("Book #"..i..": "..book)
end
Iterable mixin
Now we will use two concepts seen previously: mixins and iterators. We will try to define a behavior called Iterable which can be used on any object. Let's do it :
-- Defines an "Iterable" table
local Iterable = {
-- Adds an "items" field
items = {},
-- Adds an iterator method
iterator = function (self, index)
local index, value = next(self.items, index)
return index, value, index
end
}
-- Defines a BookStore object and use the previous Iterable table as mixin
BookStore = Object({}, Iterable)
-- Adds a constructor
function BookStore:constructor(books)
self.items = books
end
-- Creates a new BookStore instance
local bookstore = BookStore { "Romeo and Juliet", "Of mice and men", "The Wizard of Oz" }
-- prints the books in the bookstore using a for loop
-- the "iterator" method from the mixin will be used
for i, book in bookstore do
print("Book #"..i..": "..book)
end
Very interesting isn't it ?
But there's a problem, at any time someone can change the self.items
field... Let's protect this field by defining a readonly property :
-- Defines an "Iterable" table
local Iterable = {
-- Adds an iterator method
iterator = function (self, index)
local index, value = next(self.items or error("'items' field not defined"), index)
return index, value, index
end
}
-- Defines a BookStore object and use the previous Iterable table as mixin
BookStore = Object({}, Iterable)
-- Adds a constructor
function BookStore:constructor(books)
-- self.items is now a readonly property
function self:get_items()
return books
end
end
-- Creates a new BookStore instance
local bookstore = BookStore { "Romeo and Juliet", "Of mice and men", "The Wizard of Oz" }
-- prints the books in the bookstore using a for loop
-- the "iterator" method from the mixin will be used
for i, book in bookstore do
print("Book #"..i..": "..book)
end
In summary, object oriented programming with Luart offers very advanced programming paradigms by pooling the different concepts discussed here: properties, mixins, constructors, iterators...
This tutorial on object programming with Luart has now come to an end. I hope you have found it useful. Please feel free to ask for help on the Luart community forum if you have any questions.