Tutorials:Lua:ObjectOriented
Contents
Calling Notation
The first thing to understand is that you can create functions on your tables. Think of a simple table for a rectangle with a length and width and a function that calculates the area. You can go ahead and run this script in CE (go to Memory Viewer and hit `CTRL+L` to open the Lua Engine)
local rect = { length = 7, width = 9 } function calculateArea(tbl) return tbl.length * tbl.width end print('Width: '..tostring(calculateArea(rect)))
Let's look at creating the function *on the table*. This helps avoid naming
collisions and keeps the global namespace clean. Here's an example up front
showing how that works:
local rect = { length = 7, width = 9 } function rect.calculateArea(tbl) -- `.` specifies object as argument return tbl.length * tbl.width end function rect:calculateArea2() -- `:` creates assumed 'self' first argument return self.length * self.width end rect.calculateArea3 = function(tbl) return tbl.length * tbl.width end -- need to pass table with `.` print('Area: '..tostring(rect.calculateArea(rect))) print('Area2: '..tostring(rect.calculateArea2(rect))) print('Area3: '..tostring(rect.calculateArea3(rect))) -- `:` passes rect as first arg print(':Area: '..tostring(rect:calculateArea())) print(':Area2: '..tostring(rect:calculateArea2())) print(':Area3: '..tostring(rect:calculateArea3())) -- you can pass other tables as arguments even though the function is -- on the `rect` table print('Other Area:'..tostring(rect.calculateArea({ length = 4, width = 3 }))) -- p print('Other Area2:'..tostring(rect.calculateArea2({ length = 4, width = 3 }))) print('Other Area3:'..tostring(rect.calculateArea3({ length = 4, width = 3 }
Using the `.` notation, you can see that the function exists as a property on the table like any other table value like `length` and `width`. Since it is a simple function, it's not really tied to operating on the table, it takes a parameter that we called `tbl` which should be the table to operate on. When calling it with a `.`, we also need to pass the object to operate on because it is just a simple function. Although it exists on our table with length and width specified, there is no real relation between it and the table.
Now look at `calculateArea2()`. Here we use a `:` when specifying that the function exists on our `rect` table. There are no arguments, but we use `self` inside of the function. By using a `:` in the function declaration, LUA assumes there is an unspecified argument named `self` before any others.
And with `calculateArea3` we directly assign the function as a property on the table. All three of these declarations are functionally the same. The functions are simple properties on the `rect` table, they each take one argument, and they return the value of multiplying the `length` and `width` properties on that argument.
Now look at the function calls where we use a `:`. These don't pass any arguments. That is because when using a `:` when *calling* a function property, the table the function resides on is passed as the first argument. This is the basis for creating methods on objects.
Metatables
Here are some useful links for reference:
Metatables allow you to specify a second table that will handle certain actions performed on your table. This is a mostly hidden reference, though you can access it with `getmetatable(tbl)`. This lets you define a table like a class with methods and default properties and assign it as a metatable to instances without setting the functions as properties on each instance.
It also allows you to do things like intercept calls to your table as a function and to overload operators.
__index
The most useful element of a metatable is the `__index` property. If specified as a table, then any calls to properties that don't exist on your instance will attempt to be accessed on that table. Here's an example where we setup `Rectangle` as a class with the `calculateArea` method. By setting `Rectangle` as the metatable on our `rect` table, we can call `rect:calculateArea` and it will call `Rectangle.calculateArea(rect)`:
Rectangle = {} function Rectangle:calculateArea() return self.length * self.width end local rect = { length = 7, width = 9 } setmetatable(rect, { __index = Rectangle }) print('Area: '..tostring(rect:calculateArea()))
Another way to do it is to set it to a function. This function will take the table instance you are accessing a property on and the property key as arguments and should return the value. This is only called when a key does not exist directly on the table itself. A different way to accomplish the same thing as above would be to use a function that just returns the property value on the Rectangle table:
setmetatable(rect, { __index = function(tbl, key) return Rectangle[key] end })
__newindex
Similar is the metamethod `__newindex` which is called when setting a property on your table that doesn't already exist. This is much like a property write accessor in object oriented languages. It takes the table it's called on, the property key, and the value that is being set. For instance, this would log any attempts to access non-existing properties on the rect object and not actually set them, preventing code from adding properties that don't exist:
Rectangle = {} function Rectangle:calculateArea() return self.length * self.width end local rect = { length = 7, width = 9 } setmetatable(rect, { __index = Rectangle, __newindex = function(tbl, key, value) print('Rectangle: Attempt to set "'..key..'" property to '..tostring(value)) end }) rect.length = 3 print('length changed: '..tostring(rect:calculateArea())) rect.nothing = 5 print('rect.nothing was not set: '..tostring(rect.nothing))
__call
`__call` can be set to a function when your table is used like a function. This lets you write clean code for creating a new instance of your `class` instead of manually setting the metatable for an object each time. Here's an example that lets us create a new Rectangle by either calling `Rectangle.new(length, width)` or simply `Rectangle(length, width)`:
Rectangle = {} function Rectangle:calculateArea() return self.length * self.width end -- create a new instance and assign Rectangle as the metatable (like a class) function Rectangle.new(length, width) local obj = { length = length, width = width } setmetatable(obj, { __index = Rectangle }) return obj end -- allow Rectangle table to be called like a function setmetatable(Rectangle, { __call = function(tbl, length, width) -- tbl will be Rectangle return Rectangle.new(length, width) end }) local rect1 = Rectangle.new(7, 9) local rect2 = Rectangle(3, 4) print('rect1: '..tostring(rect1:calculateArea())) print('rect2: '..tostring(rect2:calculateArea()))
__lt
__lt is a function used for less than comparison, and for greater than comparison by reversing the arguments. It is really useful for sorting arrays. Here we sort the rectangles array based on their area:
Rectangle = {} function Rectangle:calculateArea() return self.length * self.width end function Rectangle.new(length, width) local obj = { length = length, width = width } setmetatable(obj, { __index = Rectangle, __lt = function(a, b) return a:calculateArea() < b:calculateArea() end }) return obj end setmetatable(Rectangle, { __call = function(tbl, length, width) -- tbl will be Rectangle return Rectangle.new(length, width) end }) local rectangles = { Rectangle(2, 5), Rectangle(7, 9), Rectangle(1, 3), Rectangle(3, 4), Rectangle(9, 10), Rectangle(2, 1) } table.sort(rectangles) for i,v in ipairs(rectangles) do print(tostring(i)..": "..tostring(v:calculateArea()).." in "..tostring(v.length).."x"..tostring(v.width)) end
other operators
- __add - Changes the behavior of the '+' operator, i.e. 'rect1 + rect2'
- __sub - Changes the behavior of the '-' operator, i.e. 'rect1 - rect2'
- __mul - Changes the behavior of the '*' operator, i.e. 'rect1 * rect2'
- __div - Changes the behavior of the '/' operator, i.e. 'rect1 / rect2'
- __mod - Changes the behavior of the '%' operator, i.e. 'rect1 % rect2'
- __unm - Changes the behavior of the unary minus '-' operator, i.e. '-rect2'
- __le - Changes the behavior of the '<=' operator (and '>=' by reversing args)
- __concat - Changes the behavior of the '..' operator, i.e. 'rect1..rect2' or 'rect1..string'
- __eq - Changes the behavior of the '==' operator (and '~='), i.e. 'rect1 == rect2'
- __tostring - Not an operator, but can change the results when using print() or tostring(), i.e. 'print(rect1)'
For example you may want to override __add and return a new Rectangle object with the length and width of two rectangles added together, or to expand both the length and width when you add a number to it. You may want to override __tostring and the concat operator .. so that you can easily print out your objects:
Rectangle = {} function Rectangle:calculateArea() return self.length * self.width end function Rectangle.new(length, width) local obj = { length = length, width = width } setmetatable(obj, { __index = Rectangle, __add = function(a, b) if type(b) == 'number' then return Rectangle(a.length + b, a.width + b) end return Rectangle(a.length + b.length, a.width + b.width) end, __concat = function(a, b) return tostring(a)..tostring(b) end, __tostring = function(a) return "[Rect("..tostring(a.length)..","..tostring(a.width)..")]" end }) return obj end setmetatable(Rectangle, { __call = function(tbl, length, width) -- tbl will be Rectangle return Rectangle.new(length, width) end }) local rect1 = Rectangle(3, 4) local rect2 = Rectangle(7, 9) local rectAdded = rect1 + rect2 -- add lengths and widths local rectExpanded = rect2 + 5 -- add 5 to length and width print('rectAdded is '..rectAdded..' and rectExpanded is '..rectExpanded)