Coding With Fun
Home Docker Django Node.js Articles Python pip guide FAQ Policy

Meta-tables and meta-methods in Lua


May 12, 2021 Lua


Table of contents


Objective

Each value in Lua can have a meta-table. A metasheet is a normal Lua table that defines the behavior of the original value under certain operations. You can change some of the behavioral characteristics of the action that acts on the value by setting a specific field in the original table of the value.

For example, when a numeric value is added as the number of operations, Lua checks its meta-table for __add function in the Field. If so, Lua calls it to perform addition.

We call the key in the meta-table an event, and the value metamethod. The event in the example above is "add" and the meta-method is a function that performs addition.

You can query the metameter of any value through the function getmetable.

In table, I can redefine the meta-methods as follows:

__add(a, b) --加法
__sub(a, b) --减法
__mul(a, b) --乘法
__div(a, b) --除法
__mod(a, b) --取模
__pow(a, b) --乘幂
__unm(a) --相反数
__concat(a, b) --连接
__len(a) --长度
__eq(a, b) --相等
__lt(a, b) --小于
__le(a, b) --小于等于
__index(a, b) --索引查询
__newindex(a, b, c) --索引更新(PS:不懂的话,后面会有讲)
__call(a, ...) --执行方法调用
__tostring(a) --字符串输出
__metatable --保护元表

Each table in Lua has its Metalable. Lua creates a new table without metalable by default

t = {}
print(getmetatable(t)) --> nil

You can use the setmetable function to set up or change the metalable of a table

t1 = {}
setmetatable(t, t1)
assert(getmetatable(t) == t1)

Any table can be a metalable for another table, and a set of related tables can share a metalable that describes their common behavior. A table can also be its own metalable (describing its private behavior).
Next, we'll cover if you want to redefine these methods.

The meta-method of the arithmetic class

Now I use the full instance code to explain in detail the use of arithmetic class meta-methods.

Set = {}
local mt = {} -- 集合的元表

-- 根据参数列表中的值创建一个新的集合
function Set.new(l)
    local set = {}
     setmetatable(set, mt)
    for _, v in pairs(l) do set[v] = true end
     return set
end

-- 并集操作
function Set.union(a, b)
    local retSet = Set.new{} -- 此处相当于Set.new({})
    for v in pairs(a) do retSet[v] = true end
    for v in pairs(b) do retSet[v] = true end
    return retSet
end

-- 交集操作
function Set.intersection(a, b)
    local retSet = Set.new{}
    for v in pairs(a) do retSet[v] = b[v] end
    return retSet
end

-- 打印集合的操作
function Set.toString(set)
     local tb = {}
     for e in pairs(set) do
          tb[#tb + 1] = e
     end
     return "{" .. table.concat(tb, ", ") .. "}"
end

function Set.print(s)
     print(Set.toString(s))
end

Now that I've defined "plus" to calculate the intersection of two collections, I need to share a meta-table with all the tables used to represent the collection, and define how to perform an addition operation in that meta-table. F irst create a regular table, prepare to be used as a meta-table for the collection, and then modify the Set.new function to set up a meta-table for the new collection each time you create it. T he code is as follows:

Set = {}
local mt = {} -- 集合的元表

-- 根据参数列表中的值创建一个新的集合
function Set.new(l)
    local set = {}
     setmetatable(set, mt)
    for _, v in pairs(l) do set[v] = true end
     return set
end

After that, all collections created by Set.new have the same meta-table, such as:

local set1 = Set.new({10, 20, 30})
local set2 = Set.new({1, 2})
print(getmetatable(set1))
print(getmetatable(set2))
assert(getmetatable(set1) == getmetatable(set2))

Finally, we need to add the meta-method to the meta-table, and the code is as follows:

mt.__add = Set.union

After that, as long as we use the "plus" symbol to find the union of two collections, it will automatically call the Set.union function and pass in two operands as arguments. F or example, the following code:

local set1 = Set.new({10, 20, 30})
local set2 = Set.new({1, 2})
local set3 = set1 + set2
Set.print(set3)

The meta-methods listed above can be redefined using the above methods. N ow there is a new problem, set1 and set2 have meta-tables, so whose meta-tables are we going to use? A lthough our sample code here uses a meta-table, in practice coding will encounter the problem I'm talking about here, for which Lua is following these steps:

  1. For binary operators, if the first operand has a meta-table, and the meta-table has the required field definition, such as the __add-dollar method definition here, then Lua takes this field as the meta-method, regardless of the second value;
  2. For binary operators, if the first operand has a meta-table, but the meta-table does not have the required field definition, such as our __add-dollar method definition here, then Lua will look for the meta-table of the second operand;
  3. Lua throws an error if neither operation number has a meta-table, or if there is no corresponding meta-method definition.

These are the rules that Lua has to deal with this problem, so what do we do in real programming?
Codes such as set3 s set1 s 8 will print the following error prompt:

lua: test.lua:16: bad argument #1 to 'pairs' (table expected, got number)

However, in the actual encoding, we can pop up the error message we defined in the following way, as follows:

function Set.union(a, b)
     if getmetatable(a) ~= mt or getmetatable(b) ~= mt then
          error("metatable error.")
     end

    local retSet = Set.new{} -- 此处相当于Set.new({})
    for v in pairs(a) do retSet[v] = true end
    for v in pairs(b) do retSet[v] = true end
    return retSet
end

When the meta-table of two operands is not the same meta-table, there is a problem when the two are in a set operation, so we can print out the error message we need.

The above summarizes the definition of meta-methods of arithmetic classes, the meta-methods of relationship classes and the meta-methods of arithmetic classes are similar, not repeated here.

__tostring method

As everyone who has written Java or C#knows, there is a tostring method in the Object class that programmers can rewrite to fulfill their needs. I n Lua, the same is true, when we are directly print (a) (a is a table), it is not allow. What to do, this time, we need to redefine ourselves The tostring meta-method allows the print to format the data that prints out the table type.
The function print always calls tostring to format the output, and when any value is formatted, tostring checks to see if there is one
Tostring's meta-method, if there is such a meta-method, tostring uses the value as an argument to call the meta-method, leaving the actual formatting operation to be done by a function referenced by the __tostring-dollar method, which eventually returns a formatted completed string. For example, the following code:

mt.__tostring = Set.toString

How to protect our "cheese" - meta-table

We will find that using getmetable can easily get metameter, use setmetable can easily modify metameter, then the risk of doing so is not too great, then how to protect our metameter from tampering? I n Lua, the functions setmetable and getmetable use a field in the metamet table to protect the metamet table, which is metatable. When we want to protect the metameter of a collection, which the user can neither see nor modify, we need to use the metatable field;

function Set.new(l)
    local set = {}
     setmetatable(set, mt)
    for _, v in pairs(l) do set[v] = true end
     mt.__metatable = "You cannot get the metatable" -- 设置完我的元表以后,不让其他人再设置
     return set
end

local tb = Set.new({1, 2})
print(tb)

print(getmetatable(tb))
setmetatable(tb, {})

The above code prints the following:

{1, 2}
You cannot get the metatable
lua: test.lua:56: cannot change a protected metatable

__index method

Remember what values are returned when we access a field that doesn't exist in a table? B y default, when we access a field that doesn't exist in a table, the result is nil. B ut that can easily change; Lua follows these steps to decide whether it's worth returning to nil or something else:

  1. When accessing a table field, if the table has this field, the corresponding value is returned directly;
  2. When table does not have this field, it prompts the interpreter to find a meta-method called __index, and then calls the corresponding meta-method, returning the value returned by the meta-method;
  3. If this meta-method is not called, the nil result is returned.

Here's a practical example of how __index used. S uppose you want to create some description windows, and each table must describe some window parameters, such as color, position, and size, that are worth it by default, so we can specify those that are different from the default worth parameters when we create window objects.

Windows = {} -- 创建一个命名空间

-- 创建默认值表
Windows.default = {x = 0, y = 0, width = 100, height = 100, color = {r = 255, g = 255, b = 255}}

Windows.mt = {} -- 创建元表

-- 声明构造函数
function Windows.new(o)
     setmetatable(o, Windows.mt)
     return o
end

-- 定义__index元方法
Windows.mt.__index = function (table, key)
     return Windows.default[key]
end

local win = Windows.new({x = 10, y = 10})
print(win.x)               -- >10 访问自身已经拥有的值
print(win.width)          -- >100 访问default表中的值
print(win.color.r)          -- >255 访问default表中的值

Based on the output of the above code, combined with the three steps mentioned above, let's look again, when print (win.x), because the win variable itself owns the x field, the value of the field it owns is printed directly; If there is no width field, then look for whether you have a meta-table, whether there is an index-corresponding meta-method in the meta-table, because there is an index meta-method, returning the value of the width field in the default table, print (win.color.r) is the same.

In real programming, __index method does not have to be a function, it can also be a table. When it's a function, Lua calls the function with table and non-existent key as arguments, which is the same as the code above, and when it's a table, Lua revisits the table in the same way, so the code above can also look like this:

-- 定义__index元方法
Windows.mt.__index = Windows.default

__newindex method

The newindex meta-method is similar to index, where the data in the table is updated, and index is used to query the data in the table. W hen assigning an index that does not exist in a table, the following steps are followed in Lua:

Lua interpreter first determines whether this table has a meta-table;

  1. If there is a meta-table, look for whether there is a __newindex-meta-method in the meta-table, if there is no meta-table, add the index directly, and then the corresponding assignment;
  2. If this __newindex method, the Lua interpreter executes it instead of performing the assignment;
  3. If this __newindex corresponds not to a function, but to a table, the Lua interpreter performs the assignment in this table, not on the original table.

So here's a problem, look at the code:

local tb1 = {}
local tb2 = {}

tb1.__newindex = tb2
tb2.__newindex = tb1

setmetatable(tb1, tb2)
setmetatable(tb2, tb1)

tb1.x = 10

Did you find anything wrong? Is it looping, in the Lua interpreter, for this problem, an error message pops up, as follows:

loop in settable

Reference blog: http://www.jellythink.com/archives/511