OpenResty:Lua唯一的数据结构table和metatable特性

LuaJIT 中只有 table 这一个数据结构,并没有区分开数组、哈 希、集合等概念,而是揉在了一起。

之前的一个例子:

local color = {first = "red", "blue", third = "green", "yellow"}
print(color["first"]) --> output: red
print(color[1]) --> output: blue
print(color["third"]) --> output: green
print(color[2]) --> output: yellow
print(color[3]) --> output: ni

color 这个 table 包含了数组和哈希,并且可以互不干扰地进行访问。比如,你可以用 ipairs 函数,只遍历数组部分的内容:

$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
for k, v in ipairs(color) do
print(k .. " " .. v)
end'

1 blue
2 yellow

table 的操作是如此重要,以至于 LuaJIT 对标准 Lua 5.1 的 table 库做了扩展,而 OpenResty 又对 LuaJIT 的 table 库做了更进一步的扩展。

table 库函数

table.getn 获取元素个数

对于序列,你用table.getn 或者一元操作符 # ,就可以正确返回元素的个数。

$ resty -e 'local t = { 1, 2, 3 }
> print(table.getn(t)) '

3


$ resty -e 'local t = { 1, 2, 3 }
print(#t) '
3

这种难以理解的函数,已经被 LuaJIT 的扩展替代,所以在 OpenResty 的环境下,除非明确知道正在获取序列的长度,否则请不要使用函数 table.getn 和一元操作符 # 。

另外,table.getn 和一元操作符 # 并不是 O(1) 的时间复杂度,而是 O(n),这也是尽量避免使用它们的另外一个理由。

table.remove 删除指定元素

它的作用是在 table 中根据下标来删除元素,也就是说只能删除 table 中数组部分的元素。

$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
>     table.remove(color, 1)
>     for k, v in pairs(color) do
>         print(v)
> end'    

yellow
green
red

这段代码会把下标为 1 的 blue 删除掉。删除 table 中的哈希部分,把 key 对应的 value 设置为 nil 即可。这样,color这个例子中,third 对应的green就被删除了。

$  resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
> color.third = nil
> for k, v in pairs(color) do
> print(v)
> end'

blue
yellow
red

table.concat 元素拼接函数

它可以按照下标,把 table 中的元素拼接起来。既然这里又是根据下标来操作的,那么显然还是针对 table 的数组部分。

$  resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
> print(table.concat(color, ", "))'

blue, yellow

这个函数还可以指定下标的起始位置来做拼接:

$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow", "orange"}
> print(table.concat(color, ", ", 2, 3))'

yellow, orange

table.insert 插入一个元素

它可以下标插入一个新的元素,影响的还是 table 的数组部分。

$ resty -e 'local color = {first = "red", "blue", third = "green", "yellow"}
> table.insert(color, 1, "orange")
> print(color[1])'

orange

也可以不指定下标,这样就会默认插入队尾。

table.insert 虽然是一个很常见的操作,但性能并不乐观。如果不根据指定下标来插入元素,那么每次都需要调用 LuaJIT 的 lj_tab_len 来获取数组的长度,以便插入队尾。正如在 table.getn 中提到的,获取 table 长度的时间复杂度为 O(n) 。

对于table.insert 操作,我们应该尽量避免在热代码中使用

resty -e 'llocal t = {}
    for i = 1, 10000 do
    table.insert(t, i)
end'

LuaJIT 的 table 扩展函数

LuaJIT 在标准 Lua 的基础上,扩展了两个很有用的 table 函数, 分别用来新建和清空一个 table。

table.new(narray, nhash) 新建 table

这个函数,会预先分配好指定的数组和哈希的空间大小, 而不是在插入元素时自增长,这也是它的两个参数 narray 和 nhash 的含义。自增长是一个代价比较高的操作,会涉及到空间分配、resize 和 rehash 等,应该尽量避免。

这个函数是扩展出来的,所以在使用它之 前,需要先 require 一下:

$ resty -e 'local new_tab = require "table.new"
> local t = new_tab(5, 0)
> for i = 1, 5 do
>     t[i] = i
> end
> print(table.concat(t,","))
> '

1,2,3,4,5

新建一个同时包含 100 个数组元素和 50 个 哈希元素的 table:

local t = new_tab(100, 50)

超出预设的空间大小,也可以正常使用,只不过性能会退化,也就失去了使用 table.new 的意义

比如下面这个例子,我们预设大小为 100,而实际上却使用了 200:

local new_tab = require "table.new"
    local t = new_tab(100, 0)
    for i = 1, 200 do
        t[i] = i
end

需要根据实际场景,来预设好 table.new 中数组和哈希空间的大小,这样才能在性能和内存占用上找到一个平衡点。

table.clear() 清空 table

它用来清空某个 table 里的所有数据,但并不会释放数组和哈希部分占用的内存。所以,它在循环利用 Lua table 时非常有用,可以避免反复创建和销毁 table 的开销。

$  resty -e 'local clear_tab =require "table.clear"
> local color = {first = "red", "blue", third = "green", "yellow"}
> clear_tab(color)
> for k, v in pairs(color) do
> print(k)
> end'

 事实上,能使用这个函数的场景并不算多,大多数情况下,我们还是应该把这个任务交给 LuaJIT GC 去完成。

OpenResty 的 table 扩展函数

OpenResty 自己维护的 LuaJIT 分支,也对 table 做了扩展,它新增了几个 API:table.isempty、table.isarray、 table.nkeys 和 table.clone。

需要注意的是,在使用这几个新增的 API 前,请记住检查你使用的 OpenResty 的版本,这些API 大都只能 在 OpenResty 1.15.8.1 之后的版本中使用。这是因为, OpenResty 在 1.15.8.1 版本之前,已经有一年左右没有发布新版本了,而这些 API 是在这个发布间隔中新增的。

table.nkeys函数是获取 table 长度的函数, 返回的是 table 的元素个数,包括数组和哈希部分的元素。因此,我们可以用它来替代 table.getn,比如 下面这样来用:

local nkeys = require "table.nkeys"
print(nkeys({})) -- 0
print(nkeys({ "a", nil, "b" })) -- 2
print(nkeys({ dog = 3, cat = 4, bird = nil })) -- 2
print(nkeys({ "a", dog = 3, cat = 4 })) -- 3

元表

由 table 引申出来元表(metatable),元表是 Lua 中独有的概念,在 实际项目中的使用非常广泛,在几乎所有的 lua-resty-* 库中,都能看到它的身影。

元表的表现行为类似于操作符重载,比如我们可以重载 __add,来计算两个 Lua 数组的并集;或者重载 __tostring,来定义转换为字符串的函数。

Lua 提供了两个处理元表的函数:

  • 第一个是setmetatable(table, metatable), 用于为一个 table 设置元表;
  • 第二个是getmetatable(table),用于获取 table 的元表。

用 setmetatable ,重新设置 version 这个 table 的 __tostring 方法,就可以打印出版本 号: 1.1.1。

$ resty -e ' local version = {
> major = 1,
> minor = 1,
> patch = 1
> }
> version = setmetatable(version, {
> __tostring = function(t)
> return string.format("%d.%d.%d", t["major"], t["minor"], t["patch"])
> end
> })
> print(tostring(version))
> '

1.1.1

  

除了 __tostring 之外,在实际项目中,我们还经常重载元表中的以下两个元方法 (metamethod)。

__index。我们在 table 中查找一个元素时,首先会直接从 table 中查询,如果没有找到,就继续到元表的 __index 中查询。

把 patch 从 version 这个 table 中去掉:

$ resty -e ' local version = {
>     major = 1,
>     minor = 1
> }
> version = setmetatable(version, {
>     __index = function(t, key)
>         if key == "patch" then
>             return 2
>         end
>     end,
>     __tostring = function(t)
>         return string.format("%d.%d.%d", t.major, t.minor, t.patch)
>     end
> })
> print(tostring(version))
> '

1.1.2

t.patch 其实获取不到值,那么就会走到 __index 这个函数中,结果就会打印出 1.1.2。

__index 不仅可以是一个函数,也可以是一个 table,如下实现的效果是一样的:

$ resty -e ' local version = {
>     major = 1,
>     minor = 1
> }
> version = setmetatable(version, {
>     __index =  {patch = 2},
>     __tostring = function(t)
>         return string.format("%d.%d.%d", t.major, t.minor, t.patch)
>     end
> })
> print(tostring(version))
> '

1.1.2

另一个元方法则是__call。它类似于仿函数,可以让 table 被调用。

是基于上面打印版本号的代码来做修改,看如何调用一个 table:

$  resty -e '
> local version = {
>     major = 1,
>     minor = 1,
>     patch = 1
> }
> 
> local function print_version(t)
>     print(string.format("%d.%d.%d", t.major, t.minor, t.patch))
> end
> 
> version = setmetatable(version,
>     {__call = print_version})
> 
> version()
> '

1.1.1

使用 setmetatable,给 version 这个 table 增加了元表,而里面的 __call 元方法指 向了函数 print_version 。那么尝试把 version 当作函数调用,这里就会执行函数 print_version。

而 getmetatable 是和 setmetatable 配对的操作,可以获取到已经设置的元表,比如下面这段代码:

$ resty -e ' local version = {
>     major = 1,
>     minor = 1
> }
> version = setmetatable(version, {
>     __index = {patch = 2},
>     __tostring = function(t)
>         return string.format("%d.%d.%d", t.major, t.minor, t.patch)
>     end
> })
> 
> print(getmetatable(version).__index.patch)
> '

2

面向对象

Lua 并不是一个面向对象(Object Orientation)的语言,但我们 可以使用 metatable 来实现 OO。

lua-resty-mysql 是 OpenResty 官方的 MySQL 客户端,里面就使用元表模拟了类和类方法,它的使用方式如下所示:

$ resty -e 'local mysql = require "resty.mysql" -- 先引⽤ lua-resty 库
local db, err = mysql:new() -- 新建⼀个类的实例
db:set_timeout(1000) -- 调⽤类的⽅法'

在调用类方法的时候,为什么是冒号而不是点号呢?

其实,在这里冒号和点号都是可以的,db:set_timeout(1000) 和 db.set_timeout(db, 1000) 是 完全等价的。冒号是 Lua 中的一个语法糖,可以省略掉函数的第一个参数 self。

local _M = { _VERSION = '0.21' } -- 使⽤ table 模拟类
local mt = { __index = _M } -- mt 即 metatable 的缩写,__index 指向类⾃⾝

-- 类的构造函数
function _M.new(self)
    local sock, err = tcp()
    if not sock then
        return nil, err
    end

    return setmetatable({ sock = sock }, mt) -- 使⽤ table 和 metatable 模拟类的实例
end

-- 类的成员函数
function _M.set_timeout(self, timeout) -- 使⽤ self 参数,获取要操作的类的实例
    local sock = self.sock
    if not sock then
        return nil, "not initialized"
    end
    return sock:settimeout(timeout)
end

_M 这个 table 模拟了一个类,初始化时,它只有 _VERSION 这一个成员变量,并在随后定义 了 _M.set_timeout 等成员函数。在 _M.new(self) 这个构造函数中,我们返回了一个 table,这个 table 的元表就是 mt,而 mt 的 __index 元方法指向了 _M,这样,返回的这个 table 就模拟了类 _M 的实 例。

猜你喜欢

转载自www.cnblogs.com/liekkas01/p/12728712.html
今日推荐