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周邊

零碎知識點


分享到:


相關文章: