编程语言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

逻辑运算符andor的也需要特别注意,它们的结果是不是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是存放在__indextable中,当在t中找不到时,去__indextable中查找。 这个特性被下面的面向对象编程用到。

面向对象

坦白讲,感觉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

pl.lapp 处理命令行参数

_G:存放全局变量的table

_G

参考

  1. OpenResty 最佳实践
  2. 编程语言Lua(一):入门介绍、学习资料、项目管理与调试方法-Lua Module

限时活动,每邀请一人即返回25元!

Copyright @2011-2018 All rights reserved. 转载请添加原文连接,合作请加微信lijiaocn或者发送邮件: lijiaocn@foxmail.com,备注网站合作 友情链接: lijiaocn github.com