Roblox, why? Are the metamethods not working?

--!strict

local MyClass = {}
MyClass.__index = MyClass

MyClass.__newindex = function(table, key, value)
	if key == "_privateField" then
		error("attempt to modify private field '" .. key .. "'", 2)
	else
		rawset(table, key, value)
	end
end

function MyClass.new(value)
	local self = setmetatable({}, MyClass)
	rawset(self, "_privateField", value)
	self.publicField = "Hello"
	return self
end

function MyClass:getPrivateField()
	return rawget(self, "_privateField")
end

function MyClass:setPrivateField(value)
	rawset(self, "_privateField", value)
end

MyClass.new("hi")._privateField = 1 -- no error
print("-----------------------")
MyClass.new()._privateField = 1 -- error

The simplest possible class. But encapsulation doesn’t work. Rather, metamethods do not work if the initial value of _privateField is not nil.

Hello,

We can handle this by using a pattern that ensures _privateField cannot be modified directly, even if it exists in the table. You can either:

Use a separate table for private fields
Use a proxy table to intercept accesses to private fields.

Here is a solution that might works

local MyClass = {}
MyClass.__index = MyClass

-- We use a metatable to handle both public and private fields
function MyClass.new(value)
    local self = setmetatable({}, MyClass)

    -- Create a table for private fields that will not be directly accessible
    local private = {
        _privateField = value
    }

    -- Use a metatable to handle access to private fields
    local proxy = setmetatable({}, {
        __index = function(_, key)
            if key == "_privateField" then
                return private[key]  -- Access the private field
            else
                return MyClass[key]  -- Access public methods or properties
            end
        end,
        __newindex = function(_, key, value)
            if key == "_privateField" then
                error("Attempt to modify private field '_privateField'", 2)
            else
                rawset(private, key, value)  -- Allow setting public fields
            end
        end
    })
    
    -- Attach proxy table to self
    self._proxy = proxy
    self.publicField = "Hello"
    return self
end

function MyClass:getPrivateField()
    return self._proxy._privateField
end

function MyClass:setPrivateField(value)
    self._proxy._privateField = value
end

-- Example usage:
local obj = MyClass.new("secret")
print(obj:getPrivateField())  -- Should print "secret"

-- Attempt to change private field directly will error
-- obj._proxy._privateField = "new value"  -- Will error: "Attempt to modify private field '_privateField'"

What’s the point of using a proxy if you can use regular meta-methods? This solution is good, however, I still don’t understand why in the past the metamethods just didn’t work.

MyClass.__newindex = function(table, key, value)
	print(key, value)
	if key == "_privateField" then
		error("attempt to modify private field '" .. key .. "'", 2)
	else
		rawset(table, key, value)
	end
end

function MyClass.new(v) 
       local self = setmetatable({}, MyClass)
       self._privateField = v
       return self
end

local abc = MyClass.new("hi")
abc._privateField = 1

theres no “_privateField” 1 in output

By the way, it’s the same with __index, if you’re interested. Why?

__newindex and __index only fire when the index that was accessed is null. In the constructor when you pass hi, _privateField is set to a non null value, therefore when you set it, it doesn’t throw. Meanwhile when you pass nothing to the constructor, the private field is null, so the metamethod is invoked and an error is thrown. Like @The_Frottik said, use a proxy table, or store private variables in a different table.

Fires when table[index] tries to be set (table[index] = value), if table[index] is nil.

Metatables

1 Like

The core of the issue is understanding how Lua’s __newindex works and why it might not fire when you expect it to.

metamethods like __newindex are triggered only when you attempt to modify a missing key in the table. If the key already exists in the table, __newindex will not be invoked—the assignment happens directly without triggering the metamethod.

MyClass.__newindex = function(table, key, value)
    print(key, value)  -- Debugging output to track assignment
    if key == "_privateField" then
        error("attempt to modify private field '" .. key .. "'", 2)
    else
        rawset(table, key, value)
    end
end

function MyClass.new(v)
    local self = setmetatable({}, MyClass)
    self._privateField = v  -- This assigns the value to _privateField in the table
    return self
end

local abc = MyClass.new("hi")
abc._privateField = 1  -- This triggers __newindex if _privateField doesn't exist

Step-by-Step Breakdown:

  1. MyClass.new(v) creates a new instance abc with self._privateField = v. This sets the _privateField key in abc directly using rawset. After this, _privateField exists in the abc table.
  2. When you try to modify abc._privateField = 1, Lua checks if the key _privateField exists in the abc table:
  • Since _privateField already exists (set in the new method), __newindex is not triggered. The assignment abc._privateField = 1 directly updates the value in the abc table without calling __newindex.
  • Why? Because __newindex only fires when you try to assign a value to a non-existent key in the table. Since _privateField already exists in the table, the metamethod doesn’t need to handle the assignment.

To Fix This: Ensure __newindex Handles Existing Keys

If you want __newindex to always be triggered, even when the field exists, you can remove the key first or avoid setting the field directly in the table. Here’s how you can modify the code:

Solution: Override the Assignment Logic Completely

You can ensure that _privateField is always handled through the metamethod by removing the direct assignment (self._privateField = v) and using a proxy approach to make all assignments go through the __newindex metamethod. Here’s an updated solution:

local MyClass = {}
MyClass.__index = MyClass

-- Metamethod for new index (setting values)
MyClass.__newindex = function(table, key, value)
    print("Attempt to modify", key, value)
    if key == "_privateField" then
        error("Attempt to modify private field '" .. key .. "'", 2)
    else
        rawset(table, key, value)
    end
end

-- Constructor function to create new instances
function MyClass.new(v)
    local self = setmetatable({}, MyClass)
    -- Don't set _privateField directly. Use the metamethod to handle it.
    rawset(self, "_privateField", v)  -- This bypasses __newindex and sets the private field
    return self
end

-- Testing the behavior
local abc = MyClass.new("hi")
abc._privateField = 1  -- This will trigger __newindex and print, followed by an error

  • Setting the Private Field with rawset: In the new function, we use rawset(self, "_privateField", v) to directly set the field bypassing the __newindex when the object is created. This ensures that when we later try to modify _privateField, it will trigger the __newindex metamethod.
  • Direct Assignment: When you later run abc._privateField = 1, since _privateField exists, Lua triggers the __newindex metamethod and calls the error function as expected.

Expected Output:

Attempt to modify _privateField 1

This error will be triggered because you are attempting to modify a field that is not allowed to be modified. The print statement in __newindex will show the attempted modification before the error is raised.

Why This Works:

  • By using rawset(self, "_privateField", v) when constructing the object, we ensure that _privateField is set in a way that still lets __newindex catch further assignments.
  • If we directly set self._privateField = v in the new function, __newindex wouldn’t trigger because _privateField would already exist in the table.

Conclusion:

The key insight here is that __newindex is only triggered for missing keys. To ensure your private field is properly encapsulated and __newindex always fires, you must either avoid direct assignment of fields or use rawset to set the fields in a way that allows the metamethod to handle further modifications.

I tried using the code you provided as a solution. It does not give an error.

This means that it is best to use tables in which the keys are instances of the class, and the value is PrivateField? Thank you.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.