Lua基础
局部定义与代码块:
使用local声明一个局部变量或局部函数,局部对象只在被声明的那个代码块中有效。
代码块:一个控制结构、一个函数体、一个chunk(一个文件或文本串)(Lua把chunk当做函数处理)
这样,可以在chunk内部声明局部函数,该函数仅在chunk内可见,并且词法定界保证了包内其他函数可以调用此函数。
在chunk内部定义多个local function并且相互调用(或定义local递归调用函数)时,最好先声明,再定义。
应该尽可能的使用局部变量(使用关键词local声明的变量),有两个好处:
1. 避免命名冲突
2. 访问局部变量的速度比全局变量更快.
可以用do..end来显示的控制local的作用范围,Lua中的do...end就相当于C++中的{},它定义了一个作用域。
多返回值函数:
第一,当作为表达式调用函数时,有以下几种情况:
1. 当调用作为表达式最后一个参数或者仅有一个参数时,根据变量个数函数尽可能多地返回多个值,不足补nil,超出舍去。
2. 其他情况下,函数调用仅返回第一个值(如果没有返回值为nil)
第二,函数调用作为函数参数被调用时,和多值赋值是相同。
第三,函数调用在表构造函数中初始化时,和多值赋值时相同。
另外,return f()这种形式,则返回“f()的返回值”:
可以使用圆括号强制使调用返回一个值。
unpack:函数多值返回的特殊函数,接受一个数组作为输入参数,返回数组的所有元素。
函数可变参数:...表示可变参数,函数体中用arg访问,同时arg还有一个域n表示参数的个数。
多值赋值经常用来交换变量,或将函数调用返回给变量:
调用函数的时候,如果参数列表为空,必须使用()表明是函数调用。
上述规则有一个例外,当函数只有一个参数并且这个参数是字符串或者表构造的时候,()可有可无。
Lua使用的函数,既可是Lua编写的,也可以是其他语言编写的,对于Lua程序员,用什么语言实现的函数使用起来都一样。(优势就在此处)
Lua中的函数是带有词法定界(lexical scoping)的第一类值(first-class values)。
第一类值指:在Lua中函数和其他值(数值、字符串)一样,函数可以被存放在变量中,也可以存放在表中,可以作为函数的参数,还可以作为函数的返回值。
词法定界指:嵌套的函数可以访问他外部函数中的变量。这一特性给Lua提供了强大的编程能力。
函数是第一类值(first-class values),也就是说函数名字比如print,实际上是一个指向函数的变量,像持有其他类型值的变量一样。
高级函数(higher-order function):以其他函数作为参数的函数,其实和普通函数没啥区别,只是参数类型是一个函数而已。
将第一类值函数应用在表中是Lua实现面向对象和包机制的关键。
函数的一般化定义应该是这样的:
foo = function (x) return 2*x end
而
function foo (x) return 2*x end
只是函数定义的一个特例。
Lua中的函数也可以没有名字,也就是匿名函数。
闭包:闭包就是一个函数以及它的upvalues;闭包就是将局部变量和一个函数进行打包维护。
因为Lua函数是带有词法界定的第一类值,闭包才得以实现,而且闭包是一个强大的功能。
upvalues:外部的局部变量(external local variable),也就是被内嵌的函数可以访问的外部那个函数中的变量。这些外部函数的局部变量和内嵌的函数就形成了一个闭包。
迭代器:Lua中迭代器是一个用闭包实现的函数。
尾调用(Tail Calls):当函数的最后一个动作是调用一个函数时,我们称这种调用为尾调用(明确什么才是尾调用,类似return func(...)这种格式的就是尾调用)。
尾调用不需要再回到调用者函数中,所以不使用额外的栈,这一点很重要,因为正确的尾调用是可以无限制的,并且不会导致栈溢出。
尤其是在写递归逻辑时,不要写成嵌套调用的形式(会不停的压栈),而建议用尾调用的方式来实现,这样效率会高很多。
Lua编译与运行:
Lua是解释性语言,但Lua会首先把代码预编译成中间码然后再执行。不要以为需要编译就不是解释型语言,Lua的编译器是语言运行时的一部分,所以,执行编译产生中间码速度会更快。
dofile/dostring和loadfile/loadstring的区别:
(1)do*会编译并执行;load*只编译代码生成中间码并且返回编译后的chunk作为一个函数,但不执行代码。
(2)load*较为灵活,发生错误时load*会返回nil和错误信息(可以打印出来)。
(3)如果要运行一个文件多次,load*只需要编译一次,但可以多次运行,do*每次都需要编译。
(4)dostring(str)等价于loadstring(str)()
Lua把chunk作为匿名函数处理,例如:chunk "a = 1",loadstring返回与其等价的function () a = 1 end
loadfile和loadstring只是编译chunk成为自己内部实现的一个匿名函数,但是这个过程没有定义函数的行为。Lua中的函数定义是发生在运行时的赋值而不是发生在编译时。也就是说loadstring以后,其中的函数还没有被定义,而dostring以后函数就定义好并且可以调用了。
loadstring编译的时候不关心词法范围,也就是说loadstring总是在全局环境中编译他的串,这一点很重要。
local i = 0
f = loadstring("i = i + 1") --使用全局变量i
g = function () i = i + 1 end --使用局部变量i
注意:chunks内部可以定义局部变量也可以返回值:
利用asset获取更多的错误信息是个好习惯。
Lua中的错误与异常:
Lua中error的处理:Lua经常作为扩展语言嵌入在别的应用中,所以不能当错误发生时简单的崩溃或者退出。相反,当错误发生时Lua结束当前的chunk并返回到应用中。
当Lua遇到不期望的情况时就会抛出错误,你也可以通过调用error函数显式地抛出错误。
当函数遇到异常有两个基本的动作:返回错误代码或者抛出错误。选择哪一种方式,没有固定的规则,不过基本的原则是:对于程序逻辑上能够避免的异常,以抛出错误的方式处理之,否则返回错误代码。
error函数:显示的抛出一个错误,终止正在执行的函数,并返回错误信息
assert函数:如果表达式出现错误,则触发一个错误,返回出错信息
assert首先检查第一个参数,若没问题,assert不做任何事情;否则,assert以第二个参数作为错误信息抛出。第二个参数是可选的。注意,assert会首先处理两个参数,
然后才调用函数,所以下面代码,无论n是否为数字,字符串连接操作总会执行:
n = io.read()
assert(tonumber(n), "invalid input: " .. n .. " is not a number")
pcall函数:在保护模式下调用函数(即发生的错误将不会反射给调用者),当调用函数成功时返回true,失败时将返回false加错误信息。pcall不会终止函数的继续运行。我们通过error抛出异常,然后通过pcall捕获之。这种机制提供了强大的能力,足以应付Lua中的各种异常和错误情况。pcall返回错误信息时,已经释放了保存错误发生情况的栈信息。
if pcall(foo) then --foo是一个函数,pcall(foo)表示调用此函数
... -- no errors while running `foo'
else
... -- `foo' raised an error: take appropriate actions
end
如果遇到内部错误(比如对一个非table的值使用索引下标访问)Lua将自己产生错误信息,否则Lua使用传递给error函数的参数作为错误信息。
xpcall函数:xpcall接受两个参数:调用函数、错误处理函数。当错误发生时,Lua会在栈释放以前调用错误处理函数,因此可以使用debug库收集错误相关信息。
debug.debug函数:debug处理函数,可以自己动手察看错误发生时的情况。
debug.trackback函数:debug处理函数,过traceback创建更多的错误信息,也是控制台解释器用来构建错误信息的函数。
动态链接库:
通常Lua不包含任何不能用标准C实现的机制,动态链接库是一个特例(动态链接库不是ANSI C的一部分,也就是说在标准C中实现动态链接是很困难的)。
我们可以将动态连接库机制视为其他机制之母:一旦我们拥有了动态连接机制,我们就可以动态的加载Lua中不存在的机制。
loadlib函数提供了Lua中动态链接库的功能。
local path = "/usr/local/lua/lib/libluasocket.so"
local f = assert(loadlib(path, "luaopen_socket"))
数据结构:
table是Lua中唯一的数据结构,可以通过table来实现其他常用的数据结构。
数组:table下标访问就是数组。习惯上,Lua下标从1开始,Lua的标准库都遵循此惯例,因此自定义的数组下标也最好从1 开始,这样才能使用标准库函数。
矩阵和多维数组:table天生具有稀疏的特性,在表示矩阵时可以节省很多空间,因为那些值为nil的元素不需要存储。
链表:lua中很少会使用这种结构,因为用其他的结构都可以替代。链表的简单实现如下所示:
--根节点 list = nil --插入一个值v list = {next = list, value = v} --遍历 local l = list while l do print(l.value) l = l.next end
队列和双向队列:虽然可以使用Lua的table库提供的insert和remove操作来实现队列,但这种方式实现的队列针对大数据量时效率太低,有效的方式是使用两个索引下标,一个表示第一个元素,另一个表示最后一个元素。
List = {} List.new = function () return {first = 0, last = -1} end List.pushleft = function (list, value) local first = list.first - 1 last.first = first list[first] = value end List.pushright = function (list, value) local last = list.last + 1 list.last = last list[last] = value end List.popleft = function (list) local first = list.first if first > list.last then error("list is empty") end local value = list[first] list[first] = nil list.first = first + 1 return value end List.popright = function (list) local last = list.last if first > last then error("list is empty") end local value = list[last] list[last] = nil list.last = last - 1 return value end
集合和包:Lua中表示集合有一个简单有效的方法,将所有集合中的元素作为下标存放在一个table里,对于给定的元素,测试该表的对应下标的元素值是否为nil,即可知道该元素是否存在。
table.concat可以将一个列表的所有串合并。
字符串合并操作,会产生赋值字符串操作,用的时候需要注意。比如str1..str2,会道指创建一个新的字符串,并将str1 和str2都拷贝过去,如果字符串很大,是比较费的操作。
Packages:
Lua并没有提供明确的机制来实现packages。然而,我们通过语言提供的基本的机制很容易实现他。主要的思想是:像标准库一样,使用表来描述package。
package和真正地命名空间的区别:首先,我们对每一个函数定义都必须显示的在前面加上包的名称。第二,同一包内的函数相互调用必须在被调用函数前指定包名。
在包定义结尾加上一个return语句:package打开的时候返回本身是一个很好的习惯。额外的返回语句并不会花费什么代价,并且提供了另一种操作package的可选方式。
私有成员:在Lua中一个传统的方法是将私有部分定义为局部变量来实现。我们野可以将package内的所有函数都声明为局部的,最后将他们放在最终的表中。但是注意:一个package的私有成员必须限制在一个文件之内,我认为这是一件好事。
自动加载:只在需要的时候加载对应的函数
table库:
table标准库提供了一些方便的函数,用来处理比如数组大小、插入/删除元素、排序等。其中有一些需要注意的点:
(1)一个常见的错误是企图对表的下标域进行排序。在一个表中,所有下标组成一个集合,但是无序的。如果你想对他们排序,必须将他们复制到一个array然后对这个array排序。
(2)对于Lua来说,数组是无序的。但是我们知道怎样去计数,因此只要我们使用排序好的下标访问数组就可以得到排好序的函数名。这就是为什么我们一直使用ipairs而不是pairs遍历数组的原因。前者使用key的顺序1、2、……,后者表的自然存储顺序。
(3)table.sort这个函数只有table是数组的时候才适用。
(4)table的instert和remove函数,如果不带位置参数,都表示操作array的最后一个元素(这样不会移动元素)。
(5)setn函数已过时,不要在lua的table中使用nil值,如果一个元素要删除,直接remove,不要用nil去代替。
string库:
Lua中的字符串是恒定不变的。String.sub函数以及Lua中其他的字符串操作函数都不会改变字符串的值,而是返回一个新的字符串。
Lua中,字符串的第一个字符索引从1开始。
string.sub(s,i,j)函数可以使用负索引,负索引从字符串的结尾向前计数:-1指向最后一个字符,-2指向倒数第二个,以此类推。第3个参数,默认为-1。
string.len(s)返回字符串s的长度;
string.rep(s, n)返回重复n次字符串s的串;
string.lower(s)将s中的大写字母转换成小写(string.upper将小写转换成大写);
string.char函数和string.byte函数用来将字符在字符和数字之间转换;
string.format在用来对字符串进行格式化的时候,特别是字符串输出,是功能强大的工具;
string.find(字符串查找);
string.gsub(全局字符串替换);
string.gfind(全局字符串查找)。
特殊字符:在模式匹配中有一些特殊字符,他们有特殊的意义,Lua中的特殊字符如下:( ) . % + - * ? [ ^ $
'%' 用作特殊字符的转义字符,转义字符 '%'不仅可以用来转义特殊字符,还可以用于所有的非字母的字符。当对一个字符有疑问的时候,为安全起见请使用转义字符转义他。只有字符串被用作模式串用于函数的时候,'%' 才作为转义字符。
[]方括号表示匹配字符集
-中横线连接符
^表示补集
Lua的字符类依赖于本地环境
模式修饰符:Lua中的模式修饰符有四个:
+ 匹配前一字符1次或多次 最长匹配
* 匹配前一字符0次或多次
- 匹配前一字符0次或多次 最短匹配
? 匹配前一字符0次或1次
Capture是这样一种机制:可以使用模式串的一部分匹配目标串的一部分。将你想捕获的模式用圆括号括起来,就指定了一个capture。
IO库:
I/O操作两种模式:
(1)简单模式:当前输入文件、当前输出文件,使用io.input和io.output来设定;
(2)完全模式:使用文件句柄,它以一种面对对象的形式,将所有的文件操作定义为文件句柄的方法。
IO库的所有函数都放在表io中。
I/O库将当前输入文件作为标准输入(stdin),将当前输出文件作为标准输出(stdout)。
io.read函数:从当前文件读入串,用参数来设定读取的模式:
(1)io.read("*all")函数从当前位置读取整个输入文件,注意是从当前位置开始读入。
(2)io.read("*line")函数返回当前输入文件的下一行(不包含最后的换行符)。
(3)io.read("*number")函数从当前输入文件中读取出一个数值。只有在该参数下read函数才返回数值,而不是字符串。可以这样使用,一次读取多个数字:
local n1, n2, n3 = io.read("*number", "*number", "*number")
(4)io.read(n)读取num个字符到串,io.read(0)函数的可以用来测试是否到达了文件末尾。如果不是返回一个空串,如果已是文件末尾返回nil。
由于Lua对长串类型值的有效管理,在Lua中使用过滤器的简单方法就是读取整个文件到串中去,处理完之后(例如使用函数gsub),接着写到输出中去。在任何情况下,都应该考虑选择使用io.read函数的 " *.all " 选项读取整个文件,然后使用gfind函数来分解。
通常Lua中读取整个文件要比一行一行的读取一个文件快的多。
尽量使用第二种方法,它避免了串联操作,消耗较少的资源
io.write(a..b..b)
io.write(a, b, c)
write和print的不同之处:
(1)write不附加任何额外的字符到输出中去;
(2)write函数是使用当前输出文件,而print始终使用标准输出;
(3)print函数会自动调用参数的tostring方法,可以显示出table、function和nil。
将我们的数据文件内容作为Lua代码写到Lua程序中去。通过使用table构造器,这些存放在Lua代码中的数据可以像其他普通的文件一样看起来引人注目。
Lua构造器,自描述数据格式,数据描述是Lua的主要应用之一。