HelloKenLee

Lua学习笔记

Posted on By KenLee

看《Programming.in.Lua.2th》的读书笔记。因为有python和c/c++基础,读的比较快,仅仅把一些需要注意的地方或者语法糖标注出来。

第一章: 开始

  1. lua -i prog.lua表示执行完prog.lua脚本后直接进入交互模式。
  2. 代码中执行dofile("file.lua")来动态执行某个文件,可用于加载库。和python的import类似。
  3. 行注释--xxxx, 块注释--[[xxx]]
  4. 显式删除一个变量只需要把那个变量赋值为nil即可。
  5. 定义全局变量_PROMPT来更改lua的交互模式提示符;定义系统环境变量LUA_INT可编写解释器执行的初始化脚本(使用@文件名 来执行某个文件;直接赋值则当做代码执行。)
  6. lua参数存在全局变量arg这个table中。按照下标可访问参数的值。
  7. 使用io.read()从stdin读入数据。
  8. lua会默认对数字和字符串类型进行互相转换, 注意性能问题。

第二章: 值与类型

  1. 复合数据类型table的本质相当于哈希表+数组, 用法相当于python的dict和list的结合。其key可以是任意的值,甚至可以是table本身。比如a={}; b={}; b[a]=1;。(疑问: table是如何用c实现的?)
  2. table永远是匿名的,因此table变量永远是引用。
  3. lua数组下标默认从1开始。
  4. #var表示求var的长度,当var为字符串时,就是求字符串长度。但当var为table时,所求的是哈希表中第一个为nil的key的下标。因此如果不是满table不建议用#求长度,实际长度可以使用table.maxn(var)求。
  5. lua的字符串永远是常量,不能通过下标修改字符串。因此修改字符串只能新建一个字符串,注意由此带来的性能问题。
  6. 使用[[多行字符串]]编写多行字符串。

第三章: 表达式

  1. lua取余定义为:a%b = a-floor(a/b)*b因此取余能对负数生效:-1 % 3 == 2,也能对小数生效:3.14 % 1 == 0.14
  2. 使用..连接两个字符串
  3. table的构造方法,推荐使用t = {[1] = 1, [2] = 2; ["key1"] = value1, ["key2"] = value2}写法。

第四章: 赋值

  1. 支持多个变量赋值,用法如python的tuple:x,y = y,x。注意如果左右两边的变量数目不一样,那么会把左边多出来的变量设为nil或者把右边多出来的值丢弃。
  2. 需要显式声明局部变量local i = 1;,不然都默认为全局变量。(注意交互模式每一行输入都为独立的一个作用域,因此交互模式的local变量在一行输入之后立刻会被销毁。如果需要多行,可以使用do...end包围代码块。)
  3. 对于需要继承全局变量的局部变量, 可以写为local foo = foo, 即使用全局变量foo去初始化局部变量foo, 在这之后该全局变量变为不可见。
  4. 条件判断写法:
if ... then
	...
elseif ... then
	...
else
	...
end

$ 5 . 使用repeat ... until(...)代替do...while(...)。注意循环体内的局部变量作用域包括until()等条件判断 $ 6 . 数字for循环, 表示变量var从a以步长step增加到b执行的语句。(a,b包头包尾,相当于<=b)

-- 打印1,3,5,7,9
for i = 1, 10, 2 do
	print(var)
end

$ 7 . 迭代器for:

-- 打印table t中的所有key
for it in pairs(t) do
	print(it)
end

$ 8 . 注意retrurnbreak只能是一个语法块的最后一句,如果要在中间return, 可以这样写:do return end

第五章: 函数

  1. 函数支持3种调用方法: foo(x); o.foo(o, x); o:foo(x);。 其中o.foo(o, x)和o:foo(x)等价。
  2. 形参和实参数量不对等的时候遵循 四.1 所述规则丢弃或置空。
  3. 允许函数返回多个值,如果接收返回值的变量不一样,按照规则丢弃返回值或置空。
  4. 使用table构造式完整接收一个函数的所有返回:t = {foo()};假设foo()返回数量是运行时确定的也没关系,但只有函数表达式作为最后一个构造元素才有意义。
  5. 特殊函数unpack(t),接收一个数组作为参数,返回数组中从下标为1开始的所有元素。
  6. 变长参数foo(...),使用表达式t = {...}获取所有参数。
  7. 使用函数select(i, ...)获取变长中第i个参数;select('#', ...)获取参数的数量。
  8. 可以通过传入一个table来实现类似python的带名字参数的函数调用。如:os.rename({old = "temp.txt", new = "temp.txt.bk"})

附:变长add函数实现:

function add(...)
	local s = 0
	for i, v in ipairs({...}) do
		s = s + v
	end
	return s
end

附:unpack()函数的实现原理:

function unpack(t, i):
	i = i or 1
	if t[i] then
		return t[i], unpack(t, i + 1)
	end
end

第六章: 深入函数

  1. 所有函数和table一样都是匿名的,函数名仅仅是指向一个函数的引用。(疑问: 那函数变量会被垃圾回收机制回收吗?)
  2. 所谓的函数定义只是语法糖,一个定义相当于一个赋值语句。如:function foo (x) retrun 2*x end; -(等价于)-> foo = function (x) return 2*x end;
  3. 使用function字段创建出来的就是一个匿名函数,类似lamda表达式,可以用于各种临时函数传入如table.sort(),也被称作高阶函数。
  4. 这种函数的一个很好的编程特性就是可以使用函数在“制造函数”(有点类似于函数版的工厂模式), 比如有一个函数需要很多参数(或者参数里面是一个函数),那么可以写一个函数来“生产”初始化好这些参数的函数,例子就是导函数。
  5. lua函数的特性导致它和python一样支持闭包(闭合函数, closure)——即一个函数可以访问其外部函数的局部变量,这时候这些变量在内部函数中既不是全局变量也不是局部变量,被成为“非全局变量”。(疑问:闭包的实现原理是什么?)
  6. “简单的说,一个closure就是一个函数加上该函数所需访问的’非局部变量’。”这些非局部变量对于每一个closure都是独立的。(对比c++的lambda表达式)
  7. 函数可以被当做变量重新定义,这种特性可以使得我们可以重新赋值lua提供的所有接口来制造一个“沙盒”,用于运行不可信的代码段。
  8. lua可以在函数内定义局部函数,只需要在函数定义前面加local关键字就好。(结合 六.1 其实就是定义了一个local变量而已)。
  9. 局部函数递归需要注意写法。
  10. 间接递归(2个函数互相递归调用)的情况需要使用明确的前向声明。
  11. lua支持尾调用消除,也就是说:当一个函数最后一个动作是调用某函数的时候,lua不会新开栈空间,这时候无论调用多少次都不会爆栈。(如: function f(x) return g(x) end)

附: 正确的局部函数递归写法:

--[[
注意不能写成:
local fact = function(n)
	...
	...
	return n * fact(n - 1) -->因为此时fact还没声明完
end
]]
local fact -- 先声明一个局部变量
fact = function(n) -- 再给其赋值
	if n == 0 then return 1
	else return n * fact(n - 1)
	end
end

附: 高阶函数/“函数工厂”:

function derivative(f, delta)
	delta = delta or 1e-4
	return functuin (x)
				return (f(x + delta) - f(x))/delta
		   end
end

第七章: 迭代器和泛型for

  1. lua的迭代器是通过闭包实现的。即不断调用某个函数,不断返回下一个位置的值。
  2. 上述迭代器每次调用都会产生一个新的闭包,但泛型for针对这点有优化,但需要无状态迭代器。参见下面等价代码。
  3. 编写迭代器也可以在迭代器参数传入要执行的函数变量,在迭代器内部执行。

附: lua迭代器闭包实现:

function values(t)
	local i = 0
	return function () i = i + 1; return t[i] end;
end

附: 泛型for的等价代码:

-- for var1, ..., varn in <explist> do <block> end 等价于:
do
	local _f, _s, _var = <explist>
	while true do:
		local var1, ..., varn = _f(_s, _var)
		_var = var1
		if _var == nil then break end
		<block>
	end
end
-- 无状态迭代器
function iter(a, i)
	i = i +1
	local v = a[i]
	if v then
		return i, v
	end
end

第八章: 编译,执行和错误

  1. 使用dofile(filename)执行外部文件代码,事实上是调用loadfile()完成的。使用loadfile(filename)加载代码,如果发生错误,会返回nil和错误信息。(对于多次执行的代码可以使用一次loadfile()减少编译次数)
  2. 使用dostring(astring)loadstring(astring)执行在字符串中的代码。
  3. loadxxx()中不涉及词法域,因此loadxxx()中不能使用非局部变量。
  4. 使用f = package.loadlib(dllpath, function_name)来载入一个c语言编译好的动态链接库的C函数,并返回一个lua函数存在f中。(不能调用任意函数,必须按照lua调用C的写法写C函数
  5. 通过error(errMsgStr)触发一个错误。
  6. 通过assert(<expr>, errMsgStr)来检查<expr>是否为真,不是则调用error函数并传入第二个参数。

第九章: 协程

  1. 使用全局的 coroutine 表中的API来创建协程
  2. 协程可以简单的理解为用户态的线程,或者理解为一个拥有上下文环境的回调函数。
  3. 协程的状态分为:suspended(调用了yield), running, dead, normal(一个协程唤醒了另一个协程而处于等待状态)
  4. 可以通过coroutine.resume(co_name, para1, para2, ...)给协程的主函数传递参数, 给一个被yield的协程传的参数可以从yield()函数的返回值获得。
  5. 协程的主函数的返回值也可以从resume()函数中获得。 简单来说,resume()yield()就是协程的调用者和被调用者的沟通桥梁。
  6. 生产者,消费者问题编程模型和过滤器。详见Producer_Consumer.lua
  7. Lua的协程是非抢占式的,也就是说,永远都只有一个协程在执行。当一个协程阻塞的时候,整个程序都会停下来。
  8. 如果需要非阻塞,则考虑自己实现线程调度器。注意解决忙等待(Busy Wait)问题。(疑问:协程和函数的差异是什么?为什么不用函数/类?)

第十一章: 各种数据结构

使用 table 构造经典数据结构。

  1. [数组]: 使用数字下标的 table 即可认为是数组。 注意索引从1开始。
  2. [矩阵]: 1) 创建数组的数组。 2) 把多维映射到一维 (和c类似)
  3. [链表]: 把 table 当做一个node, 每一个table有两个字段: val 和 next。
  4. [集合和多重集合] : 把元素当做索引+一个计数器或者布尔值即可。
  5. [字符串缓冲]: (由于字符串是常量,不断连接很费时)可以把字符串连续的存到一个table中当做字符串缓冲,用table.concat(ss)来当做缓冲。
  6. [图]: 同样把图的节点存成一个table, 里面存节点的属性(name, value…)和邻接节点集合(adj…)

第十二章: 数据文件与持久性

  1. 定义数据格式可以定义类似与 BibTex 的一种数据格式, 利用 Entry{...} 等价与 Entry({...}) 定义不同的Entry()函数即可对其进行不同的处理。 -> 自描述数据格式
  2. 串行化字符串可以使用 string.format("%q"), 其接受一个字符串并将其转化为可安全被Lua编译器读入的格式。
  3. 保存有环/有其他table引用的table的时候,可以用一个辅助的table来串行化,这个table以其他table作为key,其他table的名字作为value。

第十三章: 元表和元方法