OpenResty最佳实践笔记(4)

转自OpenResty最佳实践

元表 metatable

Lua 5.1 语言中,元表 (metatable) 的表现行为类似于 C++ 语言中的操作符重载

 例如我们可以重载 "__add" 元方法 (metamethod),来计算两个 Lua 数组的并集
 或者重载 "__index" 方法,来定义我们自己的 Hash 函数

Lua 提供了两个十分重要的用来处理元表的方法,如下:

setmetatable(table, metatable)

此方法用于为一个表设置元表。

getmetatable(table)

此方法用于获取表的元表对象。
local mytable = {}
local mymetatable = {}
setmetatable(mytable, mymetatable)

-- 简写
local mytable = setmetatable({}, {})

修改表的操作符行为

通过重载 "__add" 元方法来计算集合的并集实例:


local set1 = {10,  20, 30}
local set2 = {20, 30, 40}

local union = function (self, another)
    local set = {}
    local result = {}

    for i, j in pairs(self) do set[j] = true end
    for i, j in pairs(another) do set[j] = true end
    for i, j in pairs(set) do table.insert(result, i) end
    return result

end

setmetatable(set1, {__add = union})

local set3 = set1 + set2

for _, j in pairs(set3) do
    io.write(j.." ")
end

除了加法可以被重载之外,Lua 提供的所有操作符都可以被重载

元方法 含义
"__add" + 操作
"__sub" - 操作 其行为类似于 “add” 操作
"__mul" * 操作 其行为类似于 “add” 操作
"__div" / 操作 其行为类似于 “add” 操作
"__mod" % 操作 其行为类似于 “add” 操作
"__pow" ^ (幂)操作 其行为类似于 “add” 操作
"__unm" 一元 - 操作
"__concat" .. (字符串连接)操作
"__len" # 操作
"__eq" == 操作 函数 getcomphandler 定义了 Lua 怎样选择一个处理器来作比较操作 仅在两个对象类型相同且有对应操作相同的元方法时才起效
"__lt" < 操作
"__le" <= 操作
"__index" 取下标操作用于访问 table[key]
"__newindex" 赋值给指定下标 table[key] = value
"__tostring" 转换成字符串
"__call" 当 Lua 调用一个值时调用
"__mode" 用于弱表(week table)
"__metatable" 用于保护metatable不被访问

__tostring 元方法

--与 Java 中的 toString() 函数类似,可以实现自定义的字符串转换。

arr = {1, 2, 3, 4}
arr = setmetatable(arr, {__tostring = function (self)
    local result = '{'
    local sep = ''
    for _, i in pairs(self) do
        result = result ..sep .. i
        sep = ', '
    end
    result = result .. '}'
    return result
end})
print(arr)  --> {1, 2, 3, 4}

__call 元方法

--__call 元方法的功能类似于 C++ 中的仿函数,使得普通的表也可以被调用。

functor = {}
function func1(self, arg)
  print ("called from", arg)
end

setmetatable(functor, {__call = func1})

functor("functor")  --> called from functor
print(functor)      --> output:0x00076fc8 (后面这串数字可能不一样)

__metatable 元方法


-- 假如我们想保护我们的对象使其使用者既看不到也不能修改 metatables。我们可以对 metatable 设置了 __metatable 的值,getmetatable 将返回这个域的值,而调用 setmetatable 将会出错:
Object = setmetatable({}, {__metatable = "You cannot access here"})

print(getmetatable(Object)) --> You cannot access here
setmetatable(Object, {})    --> 引发编译器报错

____index 元方法


mytable = setmetatable({key1 = "value1"},   --原始表
  {__index = function(self, key)            --重载函数
    if key == "key2" then
      return "metatablevalue"
    end
  end
})

print(mytable.key1,mytable.key2)  --> output:value1 metatablevalue


-- __index 的元方法不需要非是一个函数,他也可以是一个表。

t = setmetatable({[1] = "hello"}, {__index = {[2] = "world"}})
print(t[1], t[2])   -->hello world

Lua 面向对象编程

Lua 中 使用函数实现面向对象

函数和相关的数据放置于同一个表中就形成了一个对象


-- account.lua

local _M = {}

local mt = { __index = _M }

function _M.deposit (self, v)
    self.balance = self.balance + v
end

function _M.withdraw (self, v)
    if self.balance > v then
        self.balance = self.balance - v
    else
        error("insufficient funds")
    end
end

function _M.new (self, balance)
    balance = balance or 0
    return setmetatable({balance = balance}, mt)
end

return _M


-- main.lua

local account = require("account")

local a = account:new()
a:deposit(100)

local b = account:new()
b:deposit(50)

print(a.balance)  --> output: 100
print(b.balance)  --> output: 50

setmetatable_M 作为新建表的原型,所以在自己的表内找不到 'deposit'、'withdraw' 这些方法和变量的时候, 便会到 __index 所指定的 _M 类型中去寻找

继承

继承可以用元表实现,它提供了在父类中查找存在的方法和变量的机制。

在 Lua 中是不推荐使用继承方式完成构造的

这样做引入的问题可能比解决的问题要多,

---------- s_base.lua
local _M = {}

local mt = { __index = _M }

function _M.upper (s)
    return string.upper(s)
end

return _M

---------- s_more.lua
local s_base = require("s_base")

local _M = {}
_M = setmetatable(_M, { __index = s_base })


function _M.lower (s)
    return string.lower(s)
end

return _M

---------- test.lua
local s_more = require("s_more")

print(s_more.upper("Hello"))   -- output: HELLO
print(s_more.lower("Hello"))   -- output: hello


成员私有性

动态语言中引入成员私有性并没有太大的必要, 反而会显著增加运行时的开销, 毕竟这种检查无法像许多静态语言那样在编译期完成

Lua 中,成员的私有性,使用类似于函数闭包的形式来实现


-- 通过工厂方法对外提供的闭包来暴露对外接口

-- 而不想暴露在外的例如 balance 成员变量,则被很好的隐藏起来

function newAccount (initialBalance)
    local self = {balance = initialBalance}
    local withdraw = function (v)
        self.balance = self.balance - v
    end
    local deposit = function (v)
        self.balance = self.balance + v
    end
    local getBalance = function () return self.balance end
    return {
        withdraw = withdraw,
        deposit = deposit,
        getBalance = getBalance
    }
end

a = newAccount(100)
a.deposit(100)
print(a.getBalance()) --> 200
print(a.balance)      --> nil

局部变量

Lua的设计有一点很奇怪,在一个 block 中的变量, 如果之前没有定义过,那么认为它是一个全局变量,而不是这个 block 的局部变量

这一点和别的语言不同

容易造成不小心覆盖了全局同名变量的错误

定义

Lua 中的局部变量要用 local 关键字来显式定义 不使用 local 显式定义的变量就是全局变量: ``
g_var = 1 – global var

local l_var = 2   -- local var

局部变量作用域

局部变量的生命周期是有限的,它的作用域仅限于声明它的块(block)。 一个块是一个控制结构的执行体、或者是一个函数的执行体再或者是一个程序块(chunk)。

x = 10
local i = 1         -- 程序块中的局部变量 i

while i <=x do
  local x = i * 2   -- while 循环体中的局部变量 x
  print(x)          -- output: 2, 4, 6, 8, ...
  i = i + 1
end

if i > 20 then
  local x           -- then 中的局部变量 x
  x = 20
  print(x + 2)      -- 如果i > 20 将会打印 22,此处的 x 是局部变量
else
  print(x)          -- 打印 10,这里 x 是全局变量
end

print(x)            -- 打印 10

  • 使用局部变量的好处

  • 局部变量可以避免因为命名问题污染了全局环境
  • local 变量的访问比全局变量更快
  • 由于局部变量出了作用域之后生命周期结束,这样可以被垃圾回收器及时释放

  • 检查模块的函数使用全局变量

lj-releng 工具来扫描 Lua 代码,定位使用 Lua 全局变量的地方

数组大小判断

table.getn(t) 等价于 #t 但计算的是数组元素,不包括 hash 键值。

而且数组是以第一个 nil 元素来判断数组结束

  • 一定不要使用 # 操作符table.getn 来计算包含 nil 的数组长度

    这是一个未定义的操作,不一定报错,但不能保证结果如你所想。

  • 删除一个数组中的元素,请使用 remove 函数,而不是用 nil 赋值


-- test.lua
local tblTest1 = { a=1, b=4, c=5 }
print("Test1 " .. table:getn(tblTest1))

local tblTest2 = { 1, nil }
print("Test2 " .. #(tblTest2))

local tblTest3 = { 1, nil, 2 }
print("Test3 " .. #(tblTest3))

local tblTest4 = { 1, nil, 2, nil }
print("Test4 " .. #(tblTest4))

local tblTest5 = { 1, nil, 2, nil, 3, nil }
print("Test5 " .. #(tblTest5))

local tblTest6 = { 1, nil, 2, nil, 3, nil, 4, nil }
print("Test6 " .. #(tblTest6))

Test1 0
Test2 1
Test3 1
Test4 1
Test5 1
Test6 1

非空判断

我们要判断一个 table 是否为 {},不能采用 #table == 0 的方式来判断


local next = next
local a = {}
local b = {name = "Bob", sex = "Male"}
local c = {"Male", "Female"}
local d = nil

print(#a)
print(#b)
print(#c)
--print(#d)    -- error

if a == nil then
    print("a == nil")
end

if b == nil then
    print("b == nil")
end

if c == nil then
    print("c == nil")
end

if d== nil then
    print("d == nil")
end

if next(a) == nil then
    print("next(a) == nil")
end

if next(b) == nil then
    print("next(b) == nil")
end

if next(c) == nil then
    print("next(c) == nil")
end

0
0
2
d == nil
next(a) == nil


-- 判断一个 table 是否为 {}

function isTableEmpty(t)
    return t == nil or next(t) == nil
end

-- 注意:next 指令是不能被 LuaJIT 的 JIT 编译优化,并且 LuaJIT 貌似没有明确计划支持这个指令优化,在不是必须的情况下,尽量少用

虚变量 _

来表示丢弃不需要的数值,仅仅起到占位的作用

-- test.lua 文件
local t = {1, 3, 5}

print("all  data:")
for i,v in ipairs(t) do
    print(i,v)
end

print("")
print("part data:")
for _,v in ipairs(t) do
    print(v)
end

点号与冒号操作符的区别

冒号操作会带入一个 self 参数,用来代表自己

点号操作,只是 内容 的展开

local str = "abcde"
print("case 1:", str:sub(1, 2))
print("case 2:", str.sub(str, 1, 2))


-- 

obj = { x = 20 }

function obj:fun1()
    print(self.x)
end

obj = { x = 20 }

function obj.fun1(self)
    print(self.x)
end

module 缓存

lua_code_cache on

local ngx_socket_tcp = ngx.socket.tcp           -- ①

local _M = { _VERSION = '0.06' }                -- ②
local mt = { __index = _M }                     -- ③

function _M.new(self)
    local sock, err = ngx_socket_tcp()          -- ④
    if not sock then
        return nil, err
    end
    return setmetatable({ sock = sock }, mt)    -- ⑤
end

function _M.set_timeout(self, timeout)
    local sock = self.sock
    if not sock then
        return nil, "not initialized"
    end

    return sock:settimeout(timeout)
end

-- ... 其他功能代码,这里简略

return _M

① 对于比较底层的模块,内部使用到的非本地函数,都需要 local 本地化,这样做的好处:

    避免命名冲突:防止外部是 require(...) 的方法调用造成全局变量污染
    访问局部变量的速度比全局变量更快、更快、更快(重要事情说三遍)
    
② 每个基础模块最好有自己 _VERSION 标识,方便后期利用 _VERSION 完成热代码部署等高级特性,也便于使用者对版本有整体意识。

③ 其实 _M 和 mt 对于不同的请求实例(require 方法得到的对象)是相同的,因为 module 会被缓存到全局环境中。
   所以在这个位置千万不要放单请求内个性信息,例如 ngx.ctx 等变量。

④ 这里需要实现的是给每个实例绑定不同的 tcp 对象,后面 setmetatable 确保了每个实例拥有自己的 socket 对象,所以必须放在 new 函数中。
   如果放在 ③ 的下面,那么这时候所有的不同实例内部将绑定了同一个 socket 对象。
   
⑤ Lua 的 module 有两种类型:支持面向对象痕迹可以保留私有属性;静态方法提供者,没有任何私有属性。
  真正起到区别作用的就是 setmetatable 函数,是否有自己的个性元表,最终导致两种不同的形态

FFI

FFI 库,是 LuaJIT 中最重要的一个扩展库。 它允许从纯 Lua 代码调用外部 C 函数,使用 C 数据结构

FFI

JIT

OpenResty 1.5.8.1 版本之后,默认捆绑的 Lua 解释器就被替换成了 LuaJIT,而不再是标准 Lua

新的解释器多了一个 JIT

LuaJIT 的运行时环境包括一个用手写汇编实现的 Lua 解释器和一个可以直接生成机器代码的 JIT 编译器

LuaJIT 官方的解释:LuaJIT is a Just-In-Time Compilerfor the Lua programming language。

Lua 代码在被执行之前总是会先被 lfn 成 LuaJIT 自己定义的字节码(Byte Code)


一开始的时候,Lua 字节码总是被 LuaJIT 的解释器解释执行。LuaJIT 的解释器会在执行字节码时同时记录一些运行时的统计信息,比如每个 Lua 函数调用入口的实际运行次数,
还有每个 Lua 循环的实际执行次数。当这些次数超过某个预设的阈值时,便认为对应的 Lua 函数入口或者对应的 Lua 循环足够的“热”,这时便会触发 JIT 编译器开始工作。

JIT 编译器会从热函数的入口或者热循环的某个位置开始尝试编译对应的 Lua 代码路径。编译的过程是把 LuaJIT 字节码先转换成 LuaJIT 自己定义的中间码(IR),
然后再生成针对目标体系结构的机器码(比如 x86_64 指令组成的机器码)。

如果当前 Lua 代码路径上的所有的操作都可以被 JIT 编译器顺利编译,则这条编译过的代码路径便被称为一个“trace”,在物理上对应一个 trace 类型的 GC 对象(即参与 Lua GC 的对象)
Buy me a 肥仔水!