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:
-
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.
- 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.