luarobloxluau

Request for explanation about objects/classes


I created a script to generate a maze and it works as expected but during coding, I discovered I should create object instances differently, so I refactored this:

local Cell = {
    index = nil,
    coordinates = Vector2.zero,
    wasVisited = false,

    markAsVisited = function (self) 
        self.wasVisited = true
    end,
    
    new = function(self, index: number, coordinates: Vector2)
        local cell = table.clone(self)
        cell.index = index
        cell.coordinates = coordinates
        
        return cell
    end,
}

return Cell

to

local Cell = {
    index = nil,
    coordinates = Vector2.zero,
    wasVisited = false
}

function Cell:new(index: number, coordinates: Vector2)
    local instance = setmetatable({}, self) 
    self.__index = self

    self.index = index
    self.coordinates = coordinates

    return instance
end

function Cell:markAsVisited()
    self.wasVisited = true
end

return Cell

and instead of

enter image description here

I get

enter image description here

can someone explain to me why?


Solution

  • Lua doesn't have classes, where you can use a constructor to create a new instance of a type. But, being a flexible language, we can achieve something like classes through clever use of metatables. So let's talk for a moment about how the __index metamethod works.

    When you ask a table for a key that doesn't exist, it will return nil. But, if you set the __index property to another table, the lookup will fall-through to that table.

    local a = {
        foo = "hello"
    }
    local b = {
        bar = "world"
    }
    
    -- ask b for "foo" which isn't defined on 'b'
    print(b.foo, b.bar) -- returns nil, "world"
    
    -- set the lookup table to 'a'
    setmetatable(b, { __index = a })
    
    -- ask again for b, which still isn't defined on 'b'
    print(b.foo, b.bar) -- returns "hello", "world"
    

    In this example, b did not become a clone of a with all of its properties, it is still a table with only bar defined. If you were to ask for foo, since it does not find it in b it then checks a to see if it can find it there. So b is not a, but it can act like it.

    Let's look at what your code is doing :

    -- create a table with some fields already defined
    local Cell = {
        index = nil,
        coordinates = Vector2.zero,
        wasVisited = false
    }
    
    -- create a "new" function that will pass the in original Cell object as the `self` variable
    function Cell:new(index: number, coordinates: Vector2)
        -- create an empty table that will reference the original Cell table
        -- note - the first time this is called, the __index metamethod hasn't been set yet
        local instance = setmetatable({}, self) 
    
        -- set Cell's __index metamethod to itself
        self.__index = self
    
        -- overwrite the original Cell's index to the passed in index
        self.index = index
    
        -- overwrite the original Cell's coordinates to the passed in coordinates
        self.coordinates = coordinates
    
        -- return an empty table that will reference all of these new values
        return instance
    end
    

    As you can see, your code is struggling to define copies of Cells because your constructor is constantly overwriting the values in the original Cell table, not assigning them to the newly created instance. Each new instance is able to access these fields and not fail, but they are always referencing the last values set.

    So, to fix it, here's what I would do :

    -- create a new table named Cell
    local Cell = {}
    
    -- set Cell's __index to itself to allow for future copies to access its functions
    Cell.__index = Cell
    
    -- set the "new" key in Cell to a function
    function Cell.new(index: number, coordinates: Vector2)
        -- create a new table and set the lookup table to be the Cell table
        -- since no functions are defined on this new table, they will fall through to access Cell's functions
        local instance = setmetatable({
            -- set some properties on the new instance table
            index = index,
            coordinates = coordinates,
            wasVisited = false,
        }, Cell) 
    
        -- return the new table
        return instance
    end
    
    -- set the "markAsVisited" key in Cell to a function
    function Cell:markAsVisited()
        -- here "self" is referring to the new instance table, not some field on Cell
        self.wasVisited = true
    end
    
    return Cell