OpenResty实战-Lua入门-Lua 面向对象编程-非空判断-正则表达式

OpenResty实战-Lua入门-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

引用代码示例:

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({balance = balance}, mt)",其中 mt 代表 { __index = _M } ,这句话值得注意。根据我们在元表这一章学到的知识,我们明白,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

成员私有性

在动态语言中引入成员私有性并没有太大的必要,反而会显著增加运行时的开销,毕竟这种检查无法像许多静态语言那样在编译期完成。下面的技巧把对象作为各方法的 upvalue,本身是很巧妙的,但会让子类继承变得困难,同时构造函数动态创建了函数,会导致构造函数无法被 JIT 编译。

在 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 面向对象编程就介绍完了,下面将介绍Lua的局部变量。

局部变量

Lua 的设计有一点很奇怪,在一个 block 中的变量,如果之前没有定义过,那么认为它是一个全局变量,而不是这个 block 的局部变量。这一点和别的语言不同。容易造成不小心覆盖了全局同名变量的错误。

定义

Lua 中的局部变量要用 local 关键字来显式定义,不使用 local 显式定义的变量就是全局变量:

g_var = 1 -- global var

local l_var = 2 -- local var

作用域

示例代码test.lua

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

使用局部变量的好处

1. 局部变量可以避免因为命名问题污染了全局环境

2. local 变量的访问比全局变量更快

3. 由于局部变量出了作用域之后生命周期结束,这样可以被垃圾回收器及时释放

常见实现如: local print = print

“尽量使用局部变量”是一种良好的编程风格。然而,初学者在使用 Lua 时,很容易忘记加上local 来定义局部变量,这时变量就会自动变成全局变量,很可能导致程序出现意想不到的问题。那么我们怎么检测哪些变量是全局变量呢?我们如何防止全局变量导致的影响呢?下面给出一段代码,利用元表的方式来自动检查全局变量,并打印必要的调试信息:

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

把下面代码保存在 foo.lua 文件中。

local _M = { _VERSION = '0.01' }

function _M.add(a, b) --两个number型变量相加

return a + b

end

function _M.update_A() --更新变量值

A = 365

end

return _M

把下面代码保存在 use_foo.lua 文件中。该文件和 foo.lua 在相同目录。

A = 360 --定义全局变量

local foo = require("foo")

local b = foo.add(A, A)

print("b = ", b)

foo.update_A()

print("A = ", A)

输出结果:

# luajit use_foo.lua

b = 720

A = 365

无论是做基础模块或是上层应用,肯定都不愿意存在这类灰色情况存在,因为它对我们系统的存在,带来很多不确定性(注意 OpenResty 会限制请求过程中全局变量的使用) 。 生产中我们是要尽力避免这种情况的出现。

Lua 上下文中应当严格避免使用自己定义的全局变量。可以使用一个 lj-releng 工具来扫描Lua 代码,定位使用 Lua 全局变量的地方。lj-releng 的相关链接:https://github.com/openresty/openresty-devel-utils/blob/master/lj-releng

如果使用 macOS 或者 Linux,可以使用下面命令安装 lj-releng :

curl -L https://github.com/openresty/openresty-devel-utils/blob/master/lj-releng > /usr/local/bin/lj-releng

chmod +x /usr/local/bin/lj-releng

Windows 用户把 lj-releng 文件所在的目录的绝对路径添加进 PATH 环境变量。然后进入你自己的 Lua 文件所在的工作目录,得到如下结果:

# lj-releng

foo.lua: 0.01 (0.01)

Checking use of Lua global variables in file foo.lua...

op no. line instruction args ; code

2 [8] SETGLOBAL 0 -1 ; A

Checking line length exceeding 80...

WARNING: No "_VERSION" or "version" field found in `use_foo.lua`.

Checking use of Lua global variables in file use_foo.lua...

op no. line instruction args ; code

2 [1] SETGLOBAL 0 -1 ; A

7 [4] GETGLOBAL 2 -1 ; A

8 [4] GETGLOBAL 3 -1 ; A

18 [8] GETGLOBAL 4 -1 ; A

Checking line length exceeding 80...

结果显示: 在 foo.lua 文件中,第 8 行设置了一个全局变量 A ; 在 use_foo.lua 文件中,没有版本信息,并且第 1 行设置了一个全局变量 A ,第 4、8 行使用了全局变量 A 。

当然,更推荐采用 luacheck 来检查项目中全局变量,见代码静态分析 一节的内容。

判断数组大小

table.getn(t) 等价于 #t 但计算的是数组元素,不包括 hash 键值。而且数组是以第一个 nil 元素来判断数组结束。 # 只计算 array 的元素个数,它实际上调用了对象的 metatable 的__len 函数。对于有 __len 方法的函数返回函数返回值,不然就返回数组成员数目。

Lua 中,数组的实现方式其实类似于 C++ 中的 map,对于数组中所有的值,都是以键值对的形式来存储(无论是显式还是隐式) ,Lua 内部实际采用哈希表和数组分别保存键值对、普通值,所以不推荐混合使用这两种赋值方式。尤其需要注意的一点是:Lua 数组中允许 nil 值的存在,但是数组默认结束标志却是 nil。这类比于 C 语言中的字符串,字符串中允许 '\0' 存在,但当读到 '\0' 时,就认为字符串已经结束了。

初始化是例外,在 Lua 相关源码中,初始化数组时首先判断数组的长度,若长度大于 0 ,并且最后一个值不为 nil,返回包括 nil 的长度;若最后一个值为 nil,则返回截至第一个非 nil 值的长度。

注意:一定不要使用 # 操作符或 table.getn 来计算包含 nil 的数组长度,这是一个未定义的操作,不一定报错,但不能保证结果如你所想。如果你要删除一个数组中的元素,请使用remove 函数,而不是用 nil 赋值。

-- test.lua

local tblTest1 = { 1, a = 2, 3 }

print("Test1 " .. #(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))

我们分别使用 Lua 和 LuaJIT 来执行一下:

➜ luajit test.lua

Test1 2

Test2 1

Test3 1

Test4 1

Test5 1

Test6 1

➜ lua test.lua

Test1 2

Test2 1

Test3 3

Test4 1

Test5 3

Test6 1

这一段的输出结果,就是这么 匪夷所思。不要在 Lua 的 table 中使用 nil 值,如果一个元素要删除,直接 remove,不要用 nil 去代替。

非空判断

大家在使用 Lua 的时候,一定会遇到不少和 nil 有关的坑吧。有时候不小心引用了一个没有赋值的变量,这时它的值默认为 nil。如果对一个 nil 进行索引的话,会导致异常。

如下:

local person = {name = "Bob", sex = "M"}

-- do something

person = nil

-- do something

print(person.name)

上面这个例子把 nil 的错误用法显而易见地展示出来,执行后,会提示下面的错误:

stdin:1:attempt to index global 'person' (a nil value)

stack traceback:

stdin:1: in main chunk

[C]: ?

然而,在实际的工程代码中,我们很难这么轻易地发现我们引用了 nil 变量。因此,在很多情况下我们在访问一些 table 型变量时,需要先判断该变量是否为 nil,例如将上面的代码改成:

local person = {name = "Bob", sex = "M"}

-- do something

person = nil

-- do something

if person ~= nil and person.name ~= nil then

print(person.name)

else

-- do something

end

对于简单类型的变量,我们可以用 if (var == nil) then 这样的简单句子来判断。但是对于 table型的 Lua 对象,就不能这么简单判断它是否为空了。一个 table 型变量的值可能是 {} ,这时它不等于 nil。我们来看下面这段代码:

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 是否为 {} ,不能采用 #table == 0 的方式来判断。可以用下面这样的方法来判断:

function isTableEmpty(t)

return t == nil or next(t) == nil

end

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

正则表达式

在 OpenResty 中,同时存在两套正则表达式规范:Lua 语言的规范和 ngx.re.* 的规范,即使您对 Lua 语言中的规范非常熟悉,我们仍不建议使用 Lua 中的正则表达式。一是因为 Lua中正则表达式的性能并不如 ngx.re.* 中的正则表达式优秀;二是 Lua 中的正则表达式并不符合 POSIX 规范,而 ngx.re.* 中实现的是标准的 POSIX 规范,后者明显更具备通用性。

Lua 中的正则表达式与 Nginx 中的正则表达式相比,有 5% - 15% 的性能损失,而且 Lua 将表达式编译成 Pattern 之后,并不会将 Pattern 缓存,而是每此使用都重新编译一遍,潜在地降低了性能。 ngx.re.* 中的正则表达式可以通过参数缓存编译过后的 Pattern,不会有类似的性能损失。

ngx.re.* 中的 o 选项,指明该参数,被编译的 Pattern 将会在工作进程中缓存,并且被当前工作进程的每次请求所共享。Pattern 缓存的上限值通过 lua_regex_cache_max_entries 来修改。

ngx.re.* 中的 j 选项,指明该参数,如果使用的 PCRE 库支持 JIT,OpenResty 会在编译 Pattern 时启用 JIT。启用 JIT 后正则匹配会有明显的性能提升。较新的平台,自带的PCRE 库均支持 JIT。如果系统自带的 PCRE 库不支持 JIT,出于性能考虑,最好自己编译一份 libpcre.so,然后在编译 OpenResty 时链接过去。要想验证当前 PCRE 库是否支持 JIT,可以这么做

1. 编译 OpenResty 时在 ./configure 中指定 --with-debug 选项

2. 在 error_log 指令中指定日志级别为 debug

3. 运行正则匹配代码,查看日志中是否有 pcre JIT compiling result: 1

即使运行在不支持 JIT 的 OpenResty 上,加上 j 选项也不会带来坏的影响。在 OpenResty官方的 Lua 库中,正则匹配至少都会带上 jo 这两个选项。

location /test {

content_by_lua_block {

local regex = [[\d+]]

-- 参数 "j" 启用 JIT 编译,参数 "o" 是开启缓存必须的

local m = ngx.re.match("hello, 1234", regex, "jo")

if m then

ngx.say(m[0])

else

ngx.say("not matched!")

end

}

}

测试结果如下:

➜ ~ curl 127.0.0.1/test

1234

另外还可以试试引入 lua-resty-core 中的正则表达式 API。这么做需要在代码里加入require 'resty.core.regex' 。 lua-resty-core 版本的 ngx.re.* ,是通过 FFI 而非 Lua/C API 来跟 OpenResty C 代码交互的。某些情况下,会带来明显的性能提升。

Lua 正则简单汇总

Lua 中正则表达式语法上最大的区别,Lua 使用 '%' 来进行转义,而其他语言的正则表达式使用 '\' 符号来进行转义。其次,Lua 中并不使用 '?' 来表示非贪婪匹配,而是定义了不同的字符来表示是否是贪婪匹配。定义如下:

符号 匹配次数 匹配模式+ 匹配前一字符 1 次或多次 非贪婪* 匹配前一字符 0 次或多次 贪婪- 匹配前一字符 0 次或多次 非贪婪? 匹配前一字符 0 次或1次 仅用于此,不用于标识是否贪婪符号 匹配模式. 任意字符%a 字母%c 控制字符%d 数字%l 小写字母%p 标点字符%s 空白符%u 大写字母%w 字母和数字%x 十六进制数字%z 代表 0 的字符

string.find 的基本应用是在目标串内搜索匹配指定的模式的串。函数如果找到匹配的串,就返回它的开始索引和结束索引,否则返回 nil。find 函数第三个参数是可选的:标示目标串中搜索的起始位置,例如当我们想实现一个迭代器时,可以传进上一次调用时的结束索引,如果返回了一个 nil 值的话,说明查找结束了.

local s = "hello world"

local i, j = string.find(s, "hello")

print(i, j) --> 1 5

string.gmatch 我们也可以使用返回迭代器的方式。

local s = "hello world from Lua"

for w in string.gmatch(s, "%a+") do

print(w)

end

-- output :

-- hello

-- world

-- from

-- Lua

string.gsub 用来查找匹配模式的串,并将使用替换串其替换掉,但并不修改原字符串,而是返回一个修改后的字符串的副本,函数有目标串,模式串,替换串三个参数,使用范例如下:

local a = "Lua is cute"

local b = string.gsub(a, "cute", "great")

print(a) --> Lua is cute

print(b) --> Lua is great

还有一点值得注意的是,'%b' 用来匹配对称的字符,而不是一般正则表达式中的单词的开始、结束。 '%b' 用来匹配对称的字符,而且采用贪婪匹配。常写为 '%bxy',x 和 y 是任意两个不同的字符;x 作为 匹配的开始,y 作为匹配的结束。比如,'%b()' 匹配以 '(' 开

始,以 ')' 结束的字符串:

print(string.gsub("a (enclosed (in) parentheses) line", "%b()", ""))

-- output: a line

后续计划内容:

Lua入门+高阶

Nginx

OpenResty

LuaRestyRedisLibrary

LuaCjsonLibrary

PostgresNginxModule

LuaNginxModule

LuaRestyDNSLibrary

LuaRestyLock

stream_lua_module

balancer_by_lua

OpenResty 与 SSL

测试

Web服务

火焰图

OpenResty周边

零碎知识点


分享到:


相關文章: