ObjectValue.Changed does not fire when :Destroy() is called on the value

Issue:
ObjectValue’s Changed event does not correctly fire its Value is destroyed or removed via garbage collection.

  1. The Value property of the ObjectValue remains even when the object it refers to is :Destroy()ed (feels unintended, or at least unintuitive + it is undocumented)
  2. The .Changed event does not fire when the object it refers to is :Destroy()ed or when garbage collection occurs.

This has been around for a while, as you can tell by this post showing a workaround

File:
ObjectValue Value Not Updating.rbxl (52.3 KB)

Setup:
image
image

An ObjectValue is in the workspace and the Value property is set to ‘Part’ before runtime - there are no other parts in the workspace other than the Baseplate.

Basic code to test
workspace.ObjectValue.Changed:Once(function(newValue)
	print("Value updated from event. New value is",newValue)
end)

task.wait(1) -- waits just to be sure it's not a loading issue

workspace.Part:Destroy()

task.wait(1)

print("Value is currently set to",workspace.ObjectValue.Value)
Code with garbage collection, no .Changed event firing when removed (either by :Destroy() or when garbage collection occurs) - set ObjectValue's Value to nil before testing
workspace.ObjectValue.Changed:Connect(function(newValue)
	print("Server - Value updated from event. New value is", tostring(newValue))
end)

local t = setmetatable({workspace.Part}, {__mode = 'v'})
workspace.ObjectValue.Value = workspace.Part

task.wait(10)

workspace.Part.Parent = nil

warn(`value before gc: {tostring(workspace.ObjectValue.Value)}`)

local lastgc, oncycle = 0, false
while task.wait() do 
	local tbl = table.create(10000)

	local newgc = gcinfo() 
	if lastgc > newgc then 
		if not oncycle then 
			warn("GC CYCLE", lastgc, newgc)
			oncycle = true 
		end 
	elseif oncycle then 
		break
	end 

	lastgc = newgc 
end

warn(`value after gc: {tostring(workspace.ObjectValue.Value)}`)
warn(#t)

(thanks to @anexpia)

Expected behavior
I would expect the Value to be immediately updated to nil when :Destroy() is called on the Value of an ObjectValue, however this is not the case.
If this was intentional behaviour, I would expect the client to not update the ObjectValue’s Value to nil, but this does seem to happen, or I would expect this behaviour to be documented somewhere.

It feels unexpected that ObjectValues wait for garbage collection rather than the :Destroy() method updating any relevant ObjectValues itself, and I would expect the Changed event to fire on :Destroy(), as well as the Value property changing.

3 Likes

i have also encountered this bug, value objects seem old, would be nice if attributes could replace them fully, such as allowing an instance value within the attributes.

1 Like

I don’t think this is a bug per-se. The object gets parented to nil, but still “exists”. Just like a local reference of it doesn’t become nil once destroyed.

I’m aware, but it still appears to be unintentional behaviour, especially considering the difference between how the server and client handle it (presumably because the ‘nil’ parent is only server-side?). If it was intentional, you’d expect the client to also still have a reference to the object, but it does not.

Not only that, but this isn’t written in the documentation for ObjectValues anywhere. It makes using ObjectValues significantly more difficult.

1 Like

This doesn’t happen outside of studio unless you are holding a reference to the part you destroyed somewhere
The value of the objectvalue becomes nil after garbage collection happens
image

code i used
workspace.ObjectValue.Changed:Connect(function(newValue)
	print("Server - Value updated from event. New value is", tostring(newValue))
end)

local t = setmetatable({workspace.Part}, {__mode = 'v'})
workspace.ObjectValue.Value = workspace.Part

task.wait(10)

workspace.Part.Parent = nil

warn(`value before gc: {tostring(workspace.ObjectValue.Value)}`)

local lastgc, oncycle = 0, false
while task.wait() do 
	local tbl = table.create(10000)

	local newgc = gcinfo() 
	if lastgc > newgc then 
		if not oncycle then 
			warn("GC CYCLE", lastgc, newgc)
			oncycle = true 
		end 
	elseif oncycle then 
		break
	end 

	lastgc = newgc 
end

warn(`value after gc: {tostring(workspace.ObjectValue.Value)}`)
warn(#t)

I assume this happens in studio because there might be a plugin - could be a userplugin or a builtin - that keeps a reference to the instance you deleted, which would explain why the value doesn’t become nil.

Yes, it appears to be a garbage collection issue. But it doesn’t appear to be a Studio-only issue. I just uploaded the place and tested it, and even then the issue was the same:


(new image to show that the part was still removed, and the value remains as the part many seconds later)

In your test you are intentionally garbage collecting - you aren’t able to reproduce this with my code at all, neither inside nor outside of Studio?

Given what you said, I also disabled all of my plugins and tried again, completely restarting Studio, but I got the same result as the first time.

Regardless, the value of the ObjectValue is NOT the same on the server and client. If it was consistent across both, that would be much more easy to manage, but because it only exists on the server (again, presumably because nil parented objects aren’t replicated), it’s nil on the client. It at least feels like unexpected behaviour, especially considering this is a very common occurrence when using ObjectValues, and it is not documented AFAIK.

With your code, I can reproduce it inside and outside of studio

But it’s expected to happen in that case since garbage collection needs to occur before the reference to the instance is removed, since all instances are saved in a weak table to prevent duplication of the userdata

Would you not say it’d be expected behaviour that using :Destroy() would remove the reference from any ObjectValues it exists in? I would expect the Changed value to fire when the object an ObjectValue refers to is destroyed.

In your example, you force garbage collection. With a lot of data and things going on in a game, garbage collection would essentially happen “at random”, so the Changed event would fire at unexpected times, despite the fact the object has already had :Destroy() called on it. It’s hard to debug especially because it’s undocumented. Feels very unintuitive if it’s expected behaviour for ObjectValues.

It’s more of a “It’s the logical thing you’d expect to happen” than “this is 100% a bug”. With the way instances are reflected to lua, I assume there is no good way of making Instance:Destroy() actually remove the references from objectvalues before garbage collection without potentially causing issues

Though Changed not firing after the reference is removed seems like it might be a bug to me.

1 Like

Sure, it’s kind of in between a bug and a documentation/clarity issue. And yeah with your code I also noticed that, which is pretty odd. Changed just never fires at all, neither on :Destroy() nor when garbage collection happens.

I do still think this is unintended behaviour though, because in a script when you hold a reference to an object, it sticks around in the code, understandably, until it’s never referred to again. ObjectValues seem to still show the reference, but without the guarantee of it sticking around. If it were intended, I’d expect it to stick around until you intentionally set ObjectValue.Value = nil or the ObjectValue is destroyed itself, rather than getting garbage collected unpredictably.

All of it just feels pretty funky with no official documentation on it and having to make workarounds (and that actual Changed bug). You would hope one of the basic Value objects would be simple and intuitive, but this is Roblox I suppose

I’ll update the post based on your findings, thanks :slight_smile:

1 Like

Hello.

The Destroy method doesn’t clear all the references to an Instance from other properties (documentation says what it does), so it doesn’t change the ObjectValue field.

ObjectValue only holds a weak reference to an Instance, so it can be garbage collected if there are no remaining references to it.
However, garbage collection doesn’t change the ObjectValue field either, the reference is still set, it just points to a dead Instance, so there’s no name to display and no Instance to return when it is accessed again.

You can suggest changes to the documentation by using the ‘Feedback’ button on the documentation page.

2 Likes