编程语言Lua(二):基本语法学习
作者: 李佶澳 转载请保留:原文地址 发布时间:2018/10/28 13:42:00
说明
在编程语言Lua(一):入门介绍、学习资料、项目管理与调试方法中给出了一些学习资料。
其中,最适合Lua语法学习的资料有两份。
第一份是Lua的设计者Roberto Ierusalimschy
写的Lua语言教程:Programming in Lua,2016年出版了第4版,覆盖了Lua 5.3。 这份资料详细讲解了Lua的语法和使用,适合初学者使用,百度云下载地址:Download:Programming in Lua, 4th Edition。
第二份是Lua各个版本的手册,例如Lua 5.3 Reference Manual。这份资料的难度比较高,高到如果你对编程语言本身没有足够的认识,根本看不懂它在讲什么。 这份资料是Lua语言的定义,可以理解为Lua语言的设计文档,是对Lua的最精确的表述。这份资料难度大,初学者不要死磕,经常看一看,慢慢地能看懂一些就可以了,在编程的世界里浸淫久了,里面的内容就会理解了。
另外,360公司的moonbingbing(真名不知道)组织编写的OpenResty 最佳实践中,对Lua也做了简短介绍。
我的学习顺序是,先把《OpenResty最佳实践》中的Lua的章节快速读了一下,然后仔细阅读《Programming in Lua》。主要是因为前者是中文的,内容也比较少,可以很快的过完,但是表述上来说,后者的更为精确,内容也细致,如果时间足够或者编程基础较弱,可以从后者看起。
下面是我的学习笔记。
关键特性记录
来自OpenResty最佳实践:
Lua语言的各个版本是`不兼容的`。
变量名没有类型,值有类型,变量名可以绑定任意类型的值。
只有一种数据结构Table,Table是数组和哈希的混合,可以用任意类型的值作为Key和Value。
函数是基本类型之一,支持匿名函数和正则尾递归(proper tail recursion)。
支持词法定界(Lexical scoping)和闭包(closure)。
支持用线程(Thread)和协程(coroutine)实现多任务。
能够在运行时载入程序文本执行。
支持通过元表(metatable)和元方法(metamethod)提供动态元机制(dynamic meta-mechainsm),允许运行时改变或扩充语法的内定语义。
支持用Table和动态元机制(dynamic meta-mechainsm)实现基于原型(prototype-based)的面向对象模型。
Lua最新的版本是5.3,但是一个用C和汇编语言编写的更高效的Lua解释器LuaJIT现在只全兼容Lua5.1,所以如果要用LuaJIT运行,注意一定要用5.1的语法。
基本情况
保留关键字和有特殊含义的符号
保留了以下关键字:
and break do else elseif
end false for function if
in local nil not or
repeat return then true until while
有特殊意义的符号总共有下面这些:
+ - * / % ^ #
== ~= <= >= < > =
( ) { } [ ]
; : , . .. ...
代码执行
一段Lua代码,无论是一个lua文件,还是命令行模式下的一行lua代码,称呼一个Chunk
。
Lua5.3支持直接在Lua命令行输入表达式,lua5.1不行:
% lua
> a = 15
> a^2
> a + 2
--> 225 --> 17
其它版本需要在前面加上“=”:
Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio
> a=15
> =a+2
17
在Lua命令行中,还可以用dofile()
函数立即加载执行一个lua文件:
> dofile("01-hello-world.lua")
Hello World 1!
>
标记符(identifier,就是变量名、函数名等所有事物的名字)可以是任意不以数字开头的字符、数字、下划线的混合。
需要避免使用下划线后面接大写字母的名字,例如_VERSION
,这种类型的标记符被Lua使用。
保留了以下关键字:
and break do else elseif
end false for function goto
if in local nil not
or repeat return then true
until while
Lua区分大小写,例如and是保留关键字,但是And、AND等不是,可以作为标记符使用。
注释用--
标记,一直作用到行尾。
多行注释,在--
后面跟随[[
,一直作用到]]
,例如:
--[[A multi-line
long comment
]]
多行注释通常采用下面的样式:
--[[
print(1)
print(2)
print(3)
--]]
Lua语句之间可以使用“;”作为分隔符,但分隔符不是必须的,可以没有,另外换行符对lua来说不具备语法上的意义。
a = 1 b = a * 2 -- ugly, but valid
变量如果不明确指定为局部的,那么就是全局变量,默认值是nil。
基本数据类型
Lua是动态类型语言,函数type()
返回一个变量或者一个值的类型:
$ lua5.1
Lua 5.1.5 Copyright (C) 1994-2012 Lua.org, PUC-Rio
> print(type("hello world"))
string
Lua的基本类型有:
nil 空类型,表示无效值,变量在被赋值前都是nil,将nil赋给一个全局变量等同将其删除
boolean 布尔类型,值为true/false,只有nil和false为“假”,其它如0和""都是真,这一点要特别特别注意!
number 数字,值为实数,Lua默认为double,LuaJIT会根据上下文选择用整型或者浮点型存储
string 字符串,支持单引号、双引号和长括号的方式
table 表,关联数组
function 函数
userdata 用来保存应用程序或者C语言写的函数库创建的新类型。
nil
nil类型需要注意的是: 变量在被赋值前都是nil,将nil赋给一个全局变量等同将其删除
boolean
布尔类型需要注意的是: 只有nil和false为“假”,其它如0和”“都是真。
number
Lua默认为double,LuaJIT会根据上下文选择用整型或者浮点型存储。
整数和浮点数的类型都是number:
> type(3) --> number
> type(3.5) --> number
> type(3.0) --> number
如果非要区分整形和浮点数,可以用math中的type函数:
> math.type(3) --> integer
> math.type(3.0) --> float
string
支持单引号、双引号和长括号的方式。
长括号分正反,正的就是两个中间有任意个“=”的[
,一个“=”表示一级,例如:
[[ 0级正长括号
[=[ 1级正长括号
[===[ 3级正长括号
它们分别和反长括号对应:
]] 0级反长括号
]=] 1级反长括号
]===] 3级反长括号
一个字符串可以以任意级别的长括号开始,直到遇到同级别的反长括号,长括号中的所有字符不被转义
,包括其它级别的长括号,例如:
> print([==[ string have a [=[ in ]=] ]==])
string have a [=[ in ]=]
Lua中字符串不能被修改
,如果要修改只能在原值的基础上新建一个字符串,也不能通过下标访问字符串中的字符。操作字符串可以用String模块
中的方法。
所有的字符串都存放在一个全局的哈希表中,相同的字符串只会存储一份
。因此创建多个内容相同的字符串,不会多占用存储空间,字符串之间的比较也是O(1)的。
table
table是Lua唯一支持的数据结构,它是一个关联数组
,一种有特殊索引的数组,索引可以是除nil以外的任意类型的值。
下面是一个table的定义和使用:
local corp = {
web = "www.google.com", --索引为字符串,key = "web",
-- value = "www.google.com"
telephone = "12345678", --索引为字符串
staff = {"Jack", "Scott", "Gary"}, --索引为字符串,值也是一个表
100876, --相当于 [1] = 100876,此时索引为数字
-- key = 1, value = 100876
100191, --相当于 [2] = 100191,此时索引为数字
[10] = 360, --直接把数字索引给出
["city"] = "Beijing" --索引为字符串
}
print(corp.web) -->output:www.google.com
print(corp["telephone"]) -->output:12345678
print(corp[2]) -->output:100191
print(corp["city"]) -->output:"Beijing"
print(corp.staff[1]) -->output:Jack
print(corp[10]) -->output:360
在Lua内部table可能使用哈希表实现的,也可能是用数组实现的,或者两者的混合,这是根据table中的数值动态决定的。
操作table的函数
table.remove
table.concat
function
函数本身也是一种基本类型,可以存储在变量中,以及通过参数传递。
local function foo()
print("in the function")
--dosomething()
local x = 10
local y = 20
return x + y
end
local a = foo --把函数赋给变量
print(a())
有名函数
就是将一个匿名函数赋给同名变量,下面的函数定义:
function foo()
end
等同于:
foo = function ()
end
array
Lua虽然只有一种数据结构table
,但是可以通过为table添加按照数字索引的方式,实现数组。
一个新数组就是一个空的table,无法指定大小,可以不停的写入。
local a = {} -- new array
for i = 1, 1000 do
a[i] = 0 end
end
通过a[i]
的方式读取,如果i超范围,返回nil。
通过#
操作符,获得数组的长度:
print(#a) --> 1000
表达式
表达式由算术运算符、关系运算符、逻辑运算符、字符串连接组成。
算术运算符
算术运算符有:
+ - * / ^(指数) %(取模)
需要特别注意的是/
表示除法,它的结果是浮点数:
print(5/10) --结果是0.2,不是0
Lua5.3引入了新的算数运算符//
,取整除法(floor division),确保返回的是一个整数:
> 3 // 2 --> 1
> 3.0 // 2 --> 1.0
> 6 // 2 --> 3
> 6.0 // 2.0 --> 3.0
> -9 // 2 --> -5
> 1.5 // 0.5 --> 3.0
关系运算符
关系运算符有:
< > <= >= == ~=(不等于)
特别注意,不等于用~=
表示。
Lua中的==
和~=
,比较的是变量绑定对象是否相同,而不是比较绑定的对象的值。
下面两个变量a、b,分别绑定的对象的值相同,但是a和b是不等的:
local a = { x = 1, y = 0}
local b = { x = 1, y = 0}
if a == b then
print("a==b")
else
print("a~=b")
end
---output:
a~=b
逻辑运算符
逻辑运算符包括:
and or not
逻辑运算符and
和or
的也需要特别注意,它们的结果是不是0和1,又不是true和false,而是运算符两边的操作数中的一个:
a and b -- 如果 a 为 nil,则返回 a,否则返回 b;
a or b -- 如果 a 为 nil,则返回 b,否则返回 a。
总结一下就是:对于and和or,返回第一个使表达式的结果确定的操作数。
not的返回结果是true或者false。
字符串拼接
字符串拼接运算符是..
,如果一个操作数是数字,数字被转换成字符串。
需要特别注意的是..
每执行一次,都会创建一个新的字符串。
如果要将多个字符串拼接起来,为了高效,应当把它们写在一个table中,然后用table.concat()
方法拼接。
local pieces = {}
for i, elem in ipairs(my_list) do
pieces[i] = my_process(elem)
end
local res = table.concat(pieces)
运算符优先级
优先级如下,由高到底排序,同一行的优先级相同:
^
not # -
* / %
+ -
..
< > <= >= == ~=
and
or
控制结构
Lua支持一下控制结构:
if
while
repeat
for
break
return
if
x = 10
if x > 0 then
print("x is a positive number")
end
x = 10
if x > 0 then
print("x is a positive number")
else
print("x is a non-positive number")
end
score = 90
if score == 100 then
print("Very good!Your score is 100")
elseif score >= 60 then
print("Congratulations, you have passed it,your score greater or equal to 60")
--此处可以添加多个elseif
else
print("Sorry, you do not pass the exam! ")
end
while
x = 1
sum = 0
while x <= 5 do
sum = sum + x
x = x + 1
end
print(sum) -->output 15
特别注意,Lua中没有continue
,支持break
。
repeat
x = 10
repeat
print(x)
until false -- 一直false,死循环
支持break
。
for
for分数字for(numeric for)和范型for(generic for)。
数字for,就是设定从一个数值,按照指定的跨度递增,直到终止值:
for i = 1, 5 do -- 从1增长到5,每一次增加1
print(i)
end
for i = 1, 10, 2 do -- 从1增长到10,每一次增加2
print(i)
end
如果跨度是负数,还可以递减:
for i = 10, 1, -1 do -- 从10递减到1,每一次减去1
print(i)
end
范型for,就是迭代器(iterator):
local a = {"a", "b", "c", "d"}
for i, v in ipairs(a) do
print("index:", i, " value:", v)
end
ipairs()
是遍历数组的迭代器函数,i是索引值,v是索引对应的数值。
支持的迭代器还有:
io.lines 迭代每行
paris 迭代table
ipairs 迭代数组元素
string.gmatch 迭代字符串中的单词
在 LuaJIT 2.1 中,ipairs() 内建函数是可以被 JIT 编译的,而 pairs() 则只能被解释执行。 因此在性能敏感的场景,应当合理安排数据结构,避免对哈希表进行遍历。 事实上,即使未来 pairs 可以被 JIT 编译,哈希表的遍历本身也不会有数组遍历那么高效,毕竟哈希表就不是为遍历而设计的数据结构。
break、return
break用于终止循环, return用于从函数中返回结果。
在函数中使用return的时候,需要注意前面加do
:
local function foo()
print("before")
do return end
print("after") -- 这一行语句永远不会执行到
end
函数
函数用关键字function
定义,默认为全局的。
全局函数保存在全局变量中,会增加性能损耗,应当尽量使用局部函数,前面加上local:
local function function_name (arc)
-- body
end
函数的定义需要在使用之前。
还可以把函数定义到某个Lua表的某个字段:
function foo.bar(a, b, c)
-- body ...
end
等同于:
foo.bar = function (a, b, c)
print(a, b, c)
end
如果参数类型不是table,参数是按值传递
的,否则传递的是table的引用。
调用函数时,如果传入的参数超过函数定义中的形参个数,多出的实参被忽略,如果传入的参数少于定义中的形参个数,没有被实参初始化的形参被用nil初始化。
变长参数用...
表示,访问的时候也使用...
:
local function func( ... ) -- 形参为 ... ,表示函数采用变长参数
local temp = {...} -- 访问的时候也要使用 ...
local ans = table.concat(temp, " ") -- 使用 table.concat 库函数对数
-- 组内容使用 " " 拼接成字符串。
print(ans)
end
table按引用传递,可以在函数修改其中的数值:
function change(arg) --change函数,改变长方形的长和宽,使其各增长一倍
arg.width = arg.width * 2 --表arg不是表rectangle的拷贝,他们是同一个表
arg.height = arg.height * 2
end -- 没有return语句了
local rectangle = { width = 20, height = 15 }
change(rectangle)
函数是多值返回的:
local function swap(a, b) -- 定义函数 swap,实现两个变量交换值
return b, a -- 按相反顺序返回变量的值
end
local x = 1
local y = 20
x, y = swap(x, y)
函数返回值个数大于接收返回值的变量的个数的时候,多余的返回值被忽略,小于的时候,多出的接收值被设置为nil。
在多变量赋值的列表表达式中,如果多值返回的函数不在最后一个,那么只有第一个返回值会被使用:
local function init() -- init 函数 返回两个值 1 和 "lua"
return 1, "lua"
end
local x, y, z = init(), 2 -- init 函数的位置不在最后,此时只返回 1
print(x, y, z) -- output 1 2 nil
local a, b, c = 2, init() -- init 函数的位置在最后,此时返回 1 和 "lua"
print(a, b, c) -- output 2 1 lua
需要注意的是,调用函数时,传入的参数也是列表表达式,遵循同样的规则:
local function init()
return 1, "lua"
end
print(init(), 2) -->output 1 2
print(2, init()) -->output 2 1 lua
如果要确保函数只返回一个值,可以用括号将函数包裹:
local function init()
return 1, "lua"
end
print((init()), 2) -->output 1 2
print(2, (init())) -->output 2 1
函数回调时,用unpack处理传入的变长参数:
local function run(x, y)
print('run', x, y)
end
local function attack(targetId)
print('targetId', targetId)
end
local function do_action(method, ...)
local args = {...} or {}
method(unpack(args, 1, table.maxn(args)))
end
do_action(run, 1, 2) -- output: run 1 2
do_action(attack, 1111) -- output: targetId 1111
模块
模块在编程语言Lua(一):入门介绍、学习资料、项目管理与调试方法-Lua Module中已经提过了,这里只记一下怎样写模块。
在 Lua 中创建一个模块最简单的方法是:创建一个 table,并将所有需要导出的函数放入其中,最后返回这个 table 就可以了。
假设模块my对应的my.lua文件内容下:
local foo={}
local function getname()
return "Lucy"
end
function foo.greeting()
print("hello " .. getname())
end
return foo
引用模块my:
local fp = require("my")
fp.greeting() -->output: hello Lucy
元表 metatable
Lua5.1中,元表相当于重新定义操作符,类似于C++中的操作符重载。
元表用函数setmetatable(table, metatable)
和函数getmetatable(table)
操作。
元表是作用在一个具体的table上的,元表中是一组重定义的元方法
:
支持的元方法有:
"__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不被访问
以重设__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
注意元方法
中第一个参数是self
。
__index
元方法有点特殊,它除了可以是一个函数,还可以是一个table:
t = setmetatable({[1] = "hello"}, {__index = {[2] = "world"}})
print(t[1], t[2]) -->hello world
上面名为t的table中,t2是存放在__index
table中,当在t中找不到时,去__index
table中查找。 这个特性被下面的面向对象编程用到。
面向对象
坦白讲,感觉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
类的方法被装在了_M
表中,而_M又被赋给了__index
,__index绑定的是mt
。
模块返回的是_M
,调用_M中的new方法的时候,模块中的mt被作为元表绑定到了传入的table,因此传入的table就可以调用mt中的方法。
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
继承
继承的实现就更麻烦了….下面是一个实现:
---------- 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
私有成员
私有成员的实现也是非常trick,下面的例子中实现了私有成员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来说太沉重了。
变量的作用域
这里有个很大的坑,在一个代码块中定义的变量,如果没有指定是局部变量,那么认为它是全局的。
局部变量必须使用local
显著标记,否则要么定义了一个全局变量,要么引用其它地方定义的同名全局变量:
g_var = 1 -- global var
local l_var = 2 -- local var
局部变量的作用域是定义它的代码库(block),例如while循环中的代码块、if中的代码块:
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
需要注意的问题
常用函数
pairs:遍历Table
pairs遍历table:
for k in pairs(cmds) do
cmds_arr[#cmds_arr+1] = k
end
pcall,xpcall:调用函数
function pcall(f, arg1, ...) end
function xpcall(f, msgh, arg1, ...) end