元表 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'
这些方法和变量的时候,
便会到 __inde
x 所指定的 _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 数据结构
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 的对象)