Lua脚本语言语法速览

  之前先上了Redis Lua的优化,这里把Lua的基本语法也顺便整理一下,后面回顾查阅会比较方便。
  其实关于Lua这个脚本语言,之前在TP-LINK工作的时候就与之擦肩而过,那时候别的小组在用他做路由器软件(基于openWrt)。后面在逛风云大大的博客时候也时常遇到过他的身影,另外在拜读开涛《亿级流量网站架构核心技术》的时候,书中描述针对大流量网站的开发、运维、管理等很多地方也是用Lua的。虽然此时对Lua的使用场景接触有限,但是看过Lua教程后发现他是一个精炼小巧的语言,现在开发圈子系统语言、脚本语言纷繁丛杂,在他们争来争去不可开交的时候,Lua反而在夹缝中找到了适合自己生存的土壤:他不求接触系统底层获取超凡执行性能;也不求建立丰富的数据结构和程序库,培养封闭自己的生态圈;而他把自己变得极为的小巧精简,用ANSI C实现因此可以运行在几乎所有的架构平台上面,也可以寄存在很多成熟的开发组件当中,当别人觉得蹩脚不方便的时候,我能够见缝插针迎难而上,真是个不错的策略啊。
  Lua程序设计这本书通篇200多页,相比于其他语言的教程可以说是很简短了,其中由易到难描述了Lua语言的方方面面。Lua要是深挖的还是有很多东西可以学的,因为我们只是用他来做插件,当前看来不会做很复杂的大规模的开发,就先看看数据结构、语句、函数这类语言的基本知识吧!

一、Lua概述

  Lua的语句不需要分隔符,所以不用纠结换行、分号等这些东西,可以按照自己的习惯写出工整方便理解的句式。Lua解释器会不断的尝试解析输入的每行内容,如果发现不能够形式一个完整的语句块,就会等到更多的输入内容。在交互式模式下,通过EOF或者os.exit()可以退出交互式Lua shell;如果将执行的语句放入文件中作为脚本,在Lua shell中可以通过dofile(“file.lua”)就可以加载这个脚本;如果只想测试某些Lua语句,可以运行lua -e “print(math.sin(12))”的形式,这样就直接运行语句,而不会启动生成交互式的Lua shell。
  在Lua程序中的任何位置可以使用两个连续的连字符(–)开始一个行注释,该注释一直延续到行的结尾,同时Lua也提供块注释的语法,他们以 –[[ 开始,止于 ]] ,不过通常为了好看实用 –]] 的样式包围起来。这个语法也通常用来注释那些不用的代码,而且重启这段代码的时候,可以在注释起始的位置再添加一个行首连字符 - 就可以了,因为这样Lua会将其解释为两个独立的行注释,而不是连接成一个块注释。

1
2
3
--- [[
print (10)
--]]

  同大多数脚本语言一样,Lua中的全局变量不需要额外的声明,只需要将一个初始值赋予一个全局变量就可以了,甚者在Lua中访问一个没有初始化的变量不会引发错误,没有初始化的变量其默认值是nil。我们一般不需要显式删除一个全局变量,如果变量的生命周期有限可以使用局部变量,而将一个nil重新赋值给一个全局变量可以显式删除这个变量。
  在命令行运行Lua脚本的时候,通过arg可以检索脚本的启动参数,因为在解释器运行脚本之前会将所有的命令行参数创建为一个名为arg的table,脚本名位于0号索引上,第一个参数隐射为索引1,此后此后类推。脚本名之前的所有选项参数都被安排在负索引上,尽管我们很少用到他们。

二、数据类型

  在Lua中有8种基本数据类型:nil、boolean、number、string、userdata、function、thread、table,使用type可以根据一个值返回其对应的类型名称并用string字符串表示。因为Lua是一个动态类型的语言,任何变量没有固定的预定义类型,它的即刻类型是根据其当前所包含的值来确定的。这里还需要尤其强调的是Lua是把函数当做“第一类值”来看待的,可以向操作其他值一样来操作一个函数值。
  (1) nil
  nil是一种类型,其值只可能是nil,其主要功能是用于同其他任何值进行区分。在Lua中,nil可以用来删除一个全局变量,而更多情况是用来表示一种“无效值”的情况。
  (2) boolean
  其值可能是true或者false。不过在Lua中,任何值都可以表示一个条件,Lua将false和nil视为假,而其他所有的值都视为真,这里尤其需要注意数字0和空字符串是被视为真的。
  (3) number
  用于表示实数,在Lua中没有单独的整数类型,Lua使用实数可以表示任何32位的整数而不会导致四舍五入的精度错误。
  (4) string
  Lua中的字符串表示为一个字符序列,可以包含任意二进制数据。在Lua中字符串是不可变值,不能直接修改字符串的某个位置的字符,而是根据条件(拼接,调用函数等)来创建一个新的字符串。
  Lua中的字符串字面值使用一对匹配的单引号或者双引号来界定的,同时中间支持C语言格式的转义字符。除此以外,Lua还支持使用一对匹配的 [[ 和 ]] 来界定一个字面值字符串,其形式和之前的块注释差不多,这种形式下的字符串不会解释其中的转移序列,尤其适合包含程序代码的字符串内容,此外第一行如果是一个空的换行,Lua会自动忽略它。
  Lua提供了运行时候的字符串和数字的自动转换,比如当在一个字符串上应用算术操作的时候,Lua会自动尝试将这个字符串转换成一个数字;同理,当Lua期望得到一个字符串的时候却提供了一个数字,那么它会自动将数字转换成字符串类型。在Lua中,…是字符串连接操作符,注意后面跟接一个数字的时候需要间隔至少一个空格,否则.会被当做小数点处理。

1
2
print("10" + 1)  --> 11
print(10 ... 20) --> 1020

  虽然这种特性看起来很酷,但是优良的程序设计者不应该依赖这项特性,在必要的时候应该使用tonumber()或者tostring()做显式的转换,当调用tonumber()试图将字符串转换为数字的时候,如果转换失败其结果是nil。
  在一个字符串签名放置 # 长度操作符,可以得到该字符串的长度值。
  (5) table
  table类型实现了关联数组(associative array),在Lua中不仅可以通过整数,还可以通过字符串或者除了nil之外的任何值来进行索引,虽然它是Lua中仅有的数据结构,但是通过简单的编程可以实现非常多其他的数据结构。实际上,Lua中的模块、包、对象都是通过table来标示的,使用.也是一种根据索引访问table中元素的方法。
  Lua中的table永远是匿名的,它是一个动态分配的对象,通过构造表达式进行创建的,Lua中不会暗中产生某个table的副本,程序只能持有一个对它的引用(或指针)来访问它,而不能声明一个table。table的形式上的“赋值”语句也仅仅是传递了一个引用,而当程序再也没有对某个table持有引用的时候,Lua的垃圾回收器会最终删除该table并复用其内存。

1
2
3
4
5
a = {}
for i=1,1000 do a[i] = i*2 end
print(a[9]) --> 18
a.x = 10 --> 可以后续添加任意元素
print(a["y") --> nil

  可以同全局变量一样,将table中的某个元素赋值为nil就可以删除这个元素。上述中 [] 索引和 . 索引意义是相同的。
  在Lua习惯中,数组的索引是从1开始的,既然约定俗称很多其他的Lua模块都依赖于这个惯例,请遵守之。在Lua中,#长度操作符可以用于返回一个数组或者线性表的最后一个索引值(跟其长度是等价的),因此,编译的语法也可以写成:

1
for i=1,#a do print(a[i]) end

  在Lua中所有没有初始化元素的索引结果都是nil,而Lua会将nil作为数组结尾的标识,所以通过上面的方式手动删除某些元素的时候,数组的中间就会含有nil,此时使用长度操作符对数组进行求值结果就会不准,这在使用的时候尤其需要注意。
  (6) function
  之前反复强调Lua中的函数是第一类值,function和传统的数据类型具有相同的权利,这表明函数可以存储在变量当中,可以通过参数传递给其他的函数,还可以作为其他函数的返回值,因此Lua中函数操作的灵活性极高。在Lua中可以重新定义一个已经存在的函数;可以删除某些函数供不可信区域不能访问。
  Lua可以调动自身Lua语言编写的函数,还可以调用C语言编写的函数。

三、表达式

  (1) 算数操作符
  Lua支持的常规算术操作符包括 + - * / ^ %,注意所有的这些操作符都是用于实数的。
  虽然他们用于整数的话和通常其他编程语言的语义相同,但是将其扩展到实数的时候,往往有些其他的效果:x%1返回的结果是x的小数部分,x-x%1返回的是x的整数部分,x-x%0.01返回的是x精确到小数点后两位的结果。
  (2) 关系操作符
  Lua支持的关系操作符包括 < > <= >= == ~=,所有的这些运算符返回的结构都是true或者false。
  ==用作相等性测试,~=用于不等性测试,如果两个操作数具有不同的类型,Lua就认为他们不相等;nil只与nil自身相等。而对于table、userdata、function的话,Lua是作引用比较的,即他们引用相同的对象才会认为他们相等,否则不同对象即使存储了相同的值,Lua也认为他们不相等。
  这里需要小心数字类型和字符串类型,这里不会做自动转换的,如果是不同类型就必定不相等。
  (3) 逻辑操作符
  逻辑操作符有and or not三种:对于and来说如果第一个操作数为假,则返回第一个操作数,否则返回第二个操作数;or如果第一个操作数为真就返回第一个操作数,否则返回第二个操作数。这里的逻辑操作符也遵循短路求值的思想,如果第一个表达式返回了,就不会去计算第二个表达式求值。
  Lua上述返回值本身的行为也可以形成一些奇特的习惯性写法,比如 x = x or v 就等价于 x = x or v –> if not x then x = v end。
  (4) table构造式
  构造式是折腾Lua唯一table数据类型的表达式,其形式如下:

1
2
3
days = { "Sun", "Mon", "Tue", "Wed", "Thr", "Fri", "Sat" }
a = { x=10, y=20 } --> a = {}; a.x = 10; a["y"] = 20;
b = { ["+"] = "add", ["-"] = "sub", ["*"] = "mul", ["/"] = "div" }

  上面days初始化了一个数组,其索引从1开始递增。第二种a的记录风格的初始化注意字符串的键没有带引号哦!

四、语句和控制结构

  (1) 赋值
  Lua支持多重赋值,可以一次性将多个值赋予多个变量,值与值、变量于变量之间使用逗号分割。赋值过程中Lua先对等号右边的元素进行求值,然后才执行赋值操作,所以可以使用这个特性方便的进行变量的交换。需要注意的是,Lua总是会将等号右边的值的个数调整到和左边变量个数保持一致,其实现规则是:如果值的个数少于变量的个数,则多余的变量会被赋值为nil;若值的个数多于变量的个数,则多余的值会被直接丢弃掉。

1
2
3
a, b = 10, 2*x
a[i], a[j] = a[j], a[i]
a, b, c = 0 -> 0, nil, nil

  (2) 局部变量和语句块
  Lua的局部变量使用local来创建,局部变量的作用域仅限于声明它的那个语句块。尽可能使用局部变量是一个好的编程喜欢,以为可以避免一些无用的名字引入到全局环境中,此外局部变量的访问也会比全局变量速度快,在超出局部变量的作用域的时候,垃圾回收期也会自动回收相关的资源。
  注意在交互式模式下,Lua会给每一行程序开启一个新的程序块,这回导致之前local创建的局部变量超出作用域而消失,在交互模式下可以使用do-end显式指明一个作用域,当然这一招在脚本中控制变量的作用域也是适用的。
  (3) 控制结构
  Lua中常用的控制语句包括if、while、repeat、for,其中if、for、while以end结束,repeat以until结束。控制语句中条件表达式可以是任意值,他们会适当的转换为真假语义。
  a. if then elseif else end

1
2
3
4
5
6
7
if op == "+" then
r = a + b
elseif op == "-" then
r = a - b
else
error("invalid op")
end

  b. while

1
2
3
4
5
local i = 1
while a[i] do
print(a[i])
i = i + 1
end

  c. repeat
  repeat-until是重复执行其循环体中的内容,直到until中的条件为假的时候才结束,测试是在循环体之后做的,所以与具体至少会被执行一次。这里需要注意的是在循环体中声明的局部变量,是可以在条件测试语句repeat中使用的。

1
2
3
4
repeat
line = io.read()
until line ~= "" -->输入第一行不为空的内容
print(line)

  d. for
  for语句包含数字型访问和泛型访问两种形式:

1
2
3
4
5
for var=exp1,exp2,exp3 do 
...
end

for i=1,f(x) do print(i) end

  上面会将var从exp1开始变化到exp2,每次变化的步长都是递增exp3,其中exp3是可选的,如果没有指定则默认步长是1。
  其中需要注意的是,for的三个控制表达式是在循环开始前一次性求值的,所以上述的f(x)只会执行一次,还有就是控制变量会被自动声明为for语句的局部变量,即var只能在循环体中可见。
  泛型for循环是通过迭代器来遍历所有值的,Lua的基础库提供了ipairs和pairs来分别迭代遍历数组元素和table中的元素:

1
2
3
for i,v in ipairs(a) do print(v) end
for k in pairs(t) do print(k) end
revDays = {} for k, v in pairs(days) do revDays[v] = k end

  迭代器访问中的变量是循环体的局部变量,而且绝对不应该对该循环变量作任何赋值操作。上面例子的最后一个可以方便的对原始table进行倒序存储。
  (4) break和return
  由于语法构造的原因,break或return只能是一个块的最后一条语句,所以他们只能是程序块的最后一条语句,或者是end、else或until前一条语句,常常需要添加额外的end或者显示使用do…end来包围之,所以看到类似奇怪的语法:

1
2
3
4
5
6
7
8
while a[i] do
if a[i] == v then break end
i = i + 1
end

function foo()
... do return end
end

五、函数

  Lua调用函数的时候,()必须有,但是有两个例外:如果函数只有一个参数,并且此参数是一个字面字符串或者table构造式,则圆括号是可以被省略的:

1
2
dofile 'a.lua'  --> dofile('a.lua')
type {} --> type({})

  Lua的形参和调用实参的数目可以不一样,这和多重赋值遵循相同的规则:若实参过多,多余的实参会被丢弃;若实参过少,则其余的形参使用nil进行初始化。
  (1) 多重返回值
  Lua的一大特性就是允许函数返回多个结果,这只需要在return语句后面列出所有需要返回的值即可。不过这些返回的值不一定能全部被利用,总结来说需要满足一些规律:只有当函数调用是一系列表达式中的最后一个元素(或只有函数调用这个元素)的时候,才能获取函数调用的所有返回值,此处的一系列表达式在Lua中包含四种情形:多重赋值、函数调用时候传入的实参列表、table的构造式、return语句。

1
2
3
4
5
6
7
8
9
10
11
12
function foo0() end
function foo1() return "a" end
function foo2() return "a", "b" end

x, y = foo1() --> x="a", y=nil
x, y = foo2() --> x="a", y="b"
x, y = foo2(), 20 --> x="a", y=20

print(foo2()) -> a b
print(foo2(), 1) --> a 1

t = {foo2()} --> t = { "a", "b" }

  (2) 变长参数
  Lua中使用 … 来表示函数可以接受可变数目的实参,当这种类型的函数被调用的时候,其参数会被聚集到一起,被称作为函数的变长参数,在函数中访问的时候也是使用 … 来进行的。具有变长参数的函数同时还可以拥有任意数目的固定参数,只不过可变参数必须是函数参数列表的最后一部分。

1
2
3
4
5
6
7
function add ( ... ) 
for i, v in iparis {...} do print(i, v) end
end

function foo( ... )
local a, b, c = ...
end

  (3) 具名实参
  Lua的函数调用和其他函数的调用机制是一样的,都是使用位置的关系将实参和形参进行配对的,但是通过特殊的技巧还可以实现具名实参的效果,防止函数实参位置错误导致不可预料的后果。实现起来就是把函数从原先接收多个参数的形式修改为只接收一个table的形式,具体来说:

1
2
3
4
5
function rename(arg)
return os.rename(arg.old, arg.new)
end

rename{old="tmp.lua", new="store.lua"}

  (4) 闭合函数(闭包)
  实际上,在Lua中函数和其他值没有差异,函数本身也是匿名的,我们通常所说的函数名不过也是持有某个函数的变量,因此这个变量也可以随意更换持有的匿名对象,而所谓的函数定义其实也是一种形式的语法糖而已:

1
2
3
4
5
6
7
8
9
function foo(x) return x*2 end
foo = function(x) return 2*x end

a = {p=print}
a.p("hello world") --> hello world

old_print = print
print = math.sin
old_print(print(1)) --> 0.8414709848079

  注意上面foo的定义方式,使用这种形式可以应用在很多算法函数中的谓词参数,不过要使得这个特性有使用价值,还依赖于Lua的一个词法域的概念,所谓的词法域就是指如果将一个函数写在另外一个函数内部,则这个位于内部的函数便可以访问外部函数中的局部变量。形如:

1
2
3
4
5
function sortbygrade (names, grades)
table.sort(names, function (n1, n2)
return grades[n1] > grades[n2]
end)
end

  首先看过之前的函数定义的本质,这里的table.sort谓语函数定义就不足为奇了,其次grades既不是这个内部函数的全部变量也不是局部变量,就将其称为非局部变量(non-local variable),此时closure闭包的概念就呼之欲出了:一个closure就是一个函数加上该函数所需访问的所有非局部变量。除了上面的情况,闭包还发挥功能的一个地方就是对某些函数重新定义,甚至是对库中预定义函数进行重新定义:

1
2
3
4
5
do
local oldSin = math.sin
local k = math.pi / 180
math.sin = function (x) return oldSin(x*k) end
end

  将老版本的math.sin丢到一个私有变量中,现在只有通过新版本sin才能访问之,现实使用中可以将其作为一个sandbox,比如只有在进行严格的鉴权、检查等操作后,才允许通过内部接口访问核心函数。

本文完!

参考