Read Only Tables

lua-users home
wiki

Difference (from prior major revision) (minor diff, author diff)

Changed: 1c1
By: KevinBaca
This article describes a technique for making a table read-only via metamethods in Lua 5.0/5.1 (for background on metamethods see the MetamethodsTutorial). The approach will only prevent the inadvertent modification of any member of a read-only table.

Changed: 3,9c3
Lua Version: 5.x

Prerequesites: Familiarity with metamethods (see MetamethodsTutorial)

This article describes a technique for making a table readonly. It will prevent the inadvertant modification of any member of a readonly table. Note, however, that members of members of a readonly table can still be modified unless they too are made readonly (see RecursiveReadOnlyTables for an alternative implementation handling recursion).

Suppose we have a table of values that we would like to remain constant.
We can define read-only (constant) tables like this:

Changed: 12,17c6,11
Directions =
{
LEFT = 1,
RIGHT = 2,
UP = 3,
DOWN = 4
Directions = readonlytable {
LEFT = 1,
RIGHT = 2,
UP = 3,
DOWN = 4,
otherstuff = {}

Changed: 21c15
We can use the metamathods __index and __newindex to ensure that these values never change.
if we define the readonlytable helper functions as follows:

Changed: 24,33c18,26
mt =
{
__index = Directions,

__newindex = function( table, key, value )
error( "Attempt to modify read-only table" )
end
}

Directions = setmetatable( {}, mt )
function readonlytable(table)
return setmetatable({}, {
__index = table,
__newindex = function(table, key, value)
error("Attempt to modify read-only table")
end,
__metatable = false
});
end

Added: 35a29,30
Note that readonlytable does not return the table originally passed to it but rather a proxy table. The proxy table is given a metatable with the metamethods __index and __newindex to ensure that the proxy table values never change. Setting the __metatable metamethod prevents tampering with the metatable itself--the client cannot obtain nor change the metatable via getmetatable and setmetatable functions.


Changed: 43c38
Note that, as mentioned above, members of members of readonly tables can still be modified. For example:
Although members of the read-only table cannot be changed, it is still possible to modify members of members of read-only tables (unless they too are explicitly made read-only tables):

Changed: 46,56c41,43
Directions =
{
LEFT = 1,
RIGHT = 2,
UP = 3,
DOWN = 4,

otherstuff =
{
}
}
> Directions.otherstuff = nil -- will fail
Attempt to modify read-only table
> Directions.otherstuff.foo = 1 -- allowed

Changed: 59c46
The table Directions.otherstuff cannot be changed or removed, but its contents can be changed or removed:
Also, rawset() and table.insert can still be used to directly modify a read-only table:

Changed: 62,64c49,52
> Directions.otherstuff = nil -- will fail
Attempt to modify read-only table
> Directions.otherstuff.foo = 1 -- OK
rawset(Directions, "LEFT", 5)
print(Directions.LEFT) -- prints 5
table.insert(Directions, 6)
print(Directions[1]) -- prints 6

Changed: 67c55
Note also that rawset() can still be used to modify a readonly table, and that access to setmetatable() will allow the user to change this behaviour. Furthermore table.insert, etc. will also directly modify the table.
If you really need to avoid that, you could implement the read-only table in C.

Changed: 69c57
Note, however, that this construct interferes with pairs, ipairs, next, the # operator, and other forms of table iteration. For example,
Furthermore, this method of creating read-only tables interferes with pairs, ipairs, next, the # operator, and other forms of table iteration. For example,

Changed: 77,81c65,66
-- prints nil!
print(next(Directions))

-- prints "0"!
print(#Directions)
print(next(Directions)) -- prints nil!
print(#Directions) -- prints "0"!

Changed: 84c69,75
See also GeneralizedPairsAndIpairs for ways to handle that.
See also GeneralizedPairsAndIpairs for ways to handle that.

Original author: KevinBaca

=== See Also ===

* RecursiveReadOnlyTables - an alternative implementation handling recursion.

This article describes a technique for making a table read-only via metamethods in Lua 5.0/5.1 (for background on metamethods see the MetamethodsTutorial). The approach will only prevent the inadvertent modification of any member of a read-only table.

We can define read-only (constant) tables like this:

Directions = readonlytable {
  LEFT   = 1,
  RIGHT  = 2,
  UP     = 3,
  DOWN   = 4,
  otherstuff = {}
}

if we define the readonlytable helper functions as follows:

function readonlytable(table)
   return setmetatable({}, {
     __index = table,
     __newindex = function(table, key, value)
                    error("Attempt to modify read-only table")
                  end,
     __metatable = false
   });
end

Note that readonlytable does not return the table originally passed to it but rather a proxy table. The proxy table is given a metatable with the metamethods __index and __newindex to ensure that the proxy table values never change. Setting the __metatable metamethod prevents tampering with the metatable itself--the client cannot obtain nor change the metatable via getmetatable and setmetatable functions.

Now if we try to modify any member of Directions we will get an error.

> Directions.LEFT = 33
Attempt to modify read-only table

Although members of the read-only table cannot be changed, it is still possible to modify members of members of read-only tables (unless they too are explicitly made read-only tables):

> Directions.otherstuff = nil    -- will fail
Attempt to modify read-only table
> Directions.otherstuff.foo = 1  -- allowed

Also, rawset() and table.insert can still be used to directly modify a read-only table:

rawset(Directions, "LEFT", 5)
print(Directions.LEFT)         -- prints 5
table.insert(Directions, 6)
print(Directions[1])           -- prints 6

If you really need to avoid that, you could implement the read-only table in C.

Furthermore, this method of creating read-only tables interferes with pairs, ipairs, next, the # operator, and other forms of table iteration. For example,

-- prints nothing!
for k,v in pairs(Directions) do
  print(k,v)
end

print(next(Directions))  -- prints nil!
print(#Directions)       -- prints "0"!

See also GeneralizedPairsAndIpairs for ways to handle that.

Original author: KevinBaca

See Also


RecentChanges · preferences
edit · history
Last edited March 8, 2016 2:26 pm GMT (diff)