Alright, but keep in mind this is pretty deep into Roblox technicals. I don’t expect you to understand this necessarily.
In Luau, there are two main kinds of reference - strong references, which stop an object from being garbage collected, and weak references, which do not stop an object from being garbage collected.
Every single reference in Luau is strong, except for when you explicitly mark either the keys or values of a table as ‘weak’ (i.e. all keys/values should be weak references) using the __mode
metatable. In this case, any value that gets garbage collected will become nil.
-- even if an object appears in this table's keys, it can still be garbage collected, at which point it would be set to nil:
local weakKeys = setmetatable({}, { __mode = "k"})
-- you can get similar behaviour for this table's values:
local weakValues = setmetatable({}, { __mode = "v"})
-- or you can have both at the same time:
local weakKeysAndValues = setmetatable({}, { __mode = "kv"})
Now for an interesting observation about instances - you can parent them into the data model, lose your reference, and get a reference back via the data model:
local reference = Instance.new("Folder")
reference.Parent = workspace
-- lose the reference...
reference = nil
-- and get one back
reference = workspace.Folder
Notice how this allows you to keep access to instances which you don’t hold any strong references to.
The way this is implemented (at least right now) is that Instance objects in Lua act like ‘pointers’ to the Instance data stored in the data model. You could, in theory, have two different Instance objects that point to the same data.
If you struggle to understand that, imagine how you can have two different tables that point to the same data stored in an array, and you’re kind of halfway there:
local data = {"Hello", "world", "from", "Luau"}
-- these two values are structurally equal, but referentially distinct
local object1 = { index = 2 }
local object2 = { index = 2 }
print(object1 == object2) -- false
print(data[object1] == data[object2]) -- true
This is exactly what happens with Instance objects - internally, they just point to some data elsewhere, and there’s nothing special or unique about any one of them. But you’ve probably noticed that this still works:
print(workspace.Folder == workspace.Folder) -- true
To make sure two instance objects that point to the same instance data are always referentially equal, Roblox keeps track of all instance objects which are still being strongly referenced. When you get an instance object from the data model, it tries to re-use an existing, strongly-referenced instance object. In other words, once it creates the first instance object for some instance data, you’ll get that same object back when you go to fetch it again.
Extending the example from earlier:
local data = {"Hello", "world", "from", "Luau"}
type Object = { index: number }
local existingObjects = setmetatable({}, { __mode = "v" })
local function getObject(withIndex: number): Object
if existingObjects[withIndex] then
return existingObjects[withIndex]
else
local obj: Object = {index = withIndex}
existingObjects[withIndex] = obj
return obj
end
end
-- these two values are now both structurally equal and referentially equal
local object1 = getObject(2)
local object2 = getObject(2)
print(object1 == object2) -- true
print(data[object1] == data[object2]) -- true
An important detail to note here is that the internal cache uses weak references - that is, if you lose all the references to an instance, Roblox may garbage collect the instance object, and so you may not get the same object back.
This leads to a rather interesting, if unintuitive, behaviour for instances in weak tables - they will stick around as long as you hold a reference to the instance in code, but they ignore whether the instance is present in the data model. Depending on your use case, this might be fine, but ultimately it’s not widely useful.