Request for explanation about objects/classes

67 Views Asked by At

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?

2

There are 2 best solutions below

1
Kylaaa On BEST ANSWER

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 :

  • change Cell.new to a period function, not a colon function. This is a style preference, but a constructor shouldn't need access to instance variables. But it does mean that you need to update the code that calls Cell:new and change them all to Cell.new
  • define your class properties on the instance. This means that each new instance technically has its own copy of those keys and they never have to reference Cell for them.
-- 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
1
Luke100000 On
function Cell:new(index: number, coordinates: Vector2)
    local instance = setmetatable({}, self) 
    self.__index = self

    self.index = index
    self.coordinates = coordinates

    return instance
end

You create a new instance, but then continue using self, which is Cell, the class. Use instance to set instance fields.