Classes and inheritance
Lesson 7.2
Lua has no class keyword, but metatables make classes easy. This lesson builds objects with methods, then shares behaviour through inheritance.
Methods and the colon
A method is a function stored in a table that works on that table. You could pass the table in by hand:
local player = { name = "Alex", health = 20 }
function player.damage(self, amount)
self.health = self.health - amount
end
player.damage(player, 5)
print(player.health) --> 15
The colon does this for you. Defining with : adds a hidden first parameter called self. Calling with : passes the table as that first argument:
local player = { name = "Alex", health = 20 }
function player:damage(amount)
self.health = self.health - amount
end
player:damage(5)
print(player.health) --> 15
player:damage(5) means exactly player.damage(player, 5). You have used this already: name:upper() is string.upper(name).
Dot or colon?
Mixing them up is the most common bug with objects. player.damage(5) passes 5 as self, and you get an error such as attempt to index a number value (local 'self'). If a function was defined with :, call it with :.
A class
A class is a table that holds the methods, and every object uses it as its __index. The objects store only their own data:
local Mob = {}
Mob.__index = Mob
function Mob.new(name, health)
local self = setmetatable({}, Mob)
self.name = name
self.health = health
return self
end
function Mob:damage(amount)
self.health = math.max(self.health - amount, 0)
end
function Mob:is_dead()
return self.health == 0
end
function Mob:__tostring()
return self.name .. " (" .. self.health .. " HP)"
end
local zombie = Mob.new("Zombie", 20)
zombie:damage(8)
print(zombie) --> Zombie (12 HP)
print(zombie:is_dead()) --> false
How it works:
Mob.__index = Mobmeans: when an object doesn’t have a key, look inMob.Mob.newcreates a table, setsMobas its metatable and fills in the fields. This is the constructor.zombie:damage(8)looks fordamageinzombie, doesn’t find it, and finds it inMobthrough__index.Mobis also the metatable, so its__tostringworks for every mob.
Each object has its own name and health, but all mobs share one copy of the methods.
Some people call the constructor with a colon, Mob:new(...), and use self instead of Mob inside it. Both styles work. Mob.new is the simpler one, because a constructor isn’t working on an existing object.
Inheritance
A subclass reuses the methods of its parent and adds or changes some. Set the parent as the subclass’s __index:
local Mob = {}
Mob.__index = Mob
function Mob.new(name, health)
return setmetatable({ name = name, health = health }, Mob)
end
function Mob:damage(amount)
self.health = math.max(self.health - amount, 0)
end
function Mob:describe()
return self.name .. " has " .. self.health .. " HP"
end
-- Creeper is a Mob
local Creeper = setmetatable({}, { __index = Mob })
Creeper.__index = Creeper
function Creeper.new(fuse)
local self = Mob.new("Creeper", 20) -- build the Mob part
self.fuse = fuse
return setmetatable(self, Creeper) -- then make it a Creeper
end
function Creeper:explode()
return self.name .. " explodes after " .. self.fuse .. " ticks!"
end
-- override a parent method, and call the parent's version
function Creeper:describe()
return Mob.describe(self) .. ", fuse " .. self.fuse
end
local c = Creeper.new(30)
c:damage(5) -- damage is found in Mob
print(c:describe()) --> Creeper has 15 HP, fuse 30
print(c:explode()) --> Creeper explodes after 30 ticks!
The lookup goes along a chain: first the object, then Creeper, then Mob. c:damage isn’t in c or Creeper, so it comes from Mob.
To call the parent’s version of an overridden method, call it through the parent with a dot and pass self: Mob.describe(self).
Checking the class
getmetatable(obj) == Creeper checks the exact class. To check “is a” through the whole chain, walk up:
local function is_a(obj, class)
local mt = getmetatable(obj)
while mt do
if mt == class then return true end
local parent = getmetatable(mt)
mt = parent and parent.__index
end
return false
end
A tiny class helper
If you write many classes, a helper removes the repetition. This one supports a parent class and a constructor called init:
local function class(parent)
local c = {}
c.__index = c
if parent then
setmetatable(c, { __index = parent })
end
c.new = function(...)
local obj = setmetatable({}, c)
if obj.init then obj:init(...) end
return obj
end
return c
end
local Animal = class()
function Animal:init(name) self.name = name end
function Animal:speak() return self.name .. " makes a sound" end
local Cat = class(Animal)
function Cat:init(name, lives)
Animal.init(self, name)
self.lives = lives
end
function Cat:speak() return self.name .. " meows" end
local a = Animal.new("Cow")
local c = Cat.new("Tom", 9)
print(a:speak()) --> Cow makes a sound
print(c:speak()) --> Tom meows
print(c.lives) --> 9
Note that metamethods such as __tostring and __add are not inherited through __index: Lua looks them up directly in the metatable. Copy them into the subclass if you need them there.
Private data
Fields in a table are always visible. Lua programmers usually mark “please don’t touch” fields with a leading underscore, such as self._cooldown. For real privacy, keep data in a closure (see More about functions) instead of in the object.
Try it
Write a class Item with Item.new(id, count), a method add(n) that never lets the count go above 64 and returns how many didn’t fit, and a __tostring like minecraft:arrow x16. Then make a subclass Tool with a durability field, a use() method that lowers it by 1, and a count that is always 1.
Show a solution
local Item = {}
Item.__index = Item
function Item.new(id, count)
return setmetatable({ id = id, count = count or 1 }, Item)
end
function Item:add(n)
local space = 64 - self.count
local added = math.min(n, space)
self.count = self.count + added
return n - added
end
function Item:__tostring()
return self.id .. " x" .. self.count
end
local Tool = setmetatable({}, { __index = Item })
Tool.__index = Tool
Tool.__tostring = Item.__tostring -- metamethods aren't inherited
function Tool.new(id, durability)
local self = Item.new(id, 1)
self.durability = durability
return setmetatable(self, Tool)
end
function Tool:add(n)
return n -- tools don't stack: nothing fits
end
function Tool:use()
self.durability = self.durability - 1
end
local arrows = Item.new("minecraft:arrow", 60)
print(arrows:add(10)) --> 6
print(arrows) --> minecraft:arrow x64
local pick = Tool.new("minecraft:iron_pickaxe", 250)
pick:use()
print(pick, pick.durability) --> minecraft:iron_pickaxe x1 249
Recap: obj:method() passes obj as self, a class is a table used as __index for its objects, and a subclass sets its parent as its own __index.