《lua程序设计》学习记录
前言
为了配合redis做原子操作学习lua, 正在读《Lua程序设计》, 此书短小精悍, 应该几日就能读完, 记下这篇笔记便于回忆。正在持续施工中…
A Very Beginning: Hello World
1 | print("Hello World") |
学习编程语言的开始, 感觉回到了python。lua用--
注释行, 用--[[...]]--
注释多行。
一个简洁的例子: fact
下面简要演示了lua语言的语法图景, 有个简单的印象:
1 | function fact(n) |
词法规范的特殊规则
- 避免用一个下划线跟着一个或多个大写字母作为标识符, 因为这类标识符有特殊的用途。
在命令行里打lua
, 然后就进入了lua的交互式模式, 此时输入_PROMPT = '$ '
, 这样你的lua交互模式的命令提示符就变成$
了, 这是特殊用途之一。
还有其它的特殊用途我现在还不知道(TODO)。
- 不用一个下划线作为标识符, 一个下划线作为”哑变量”使用。哑变量是什么我现在不知道(TODO)。
解除多行注释
下面介绍了一个奇技淫巧:
1 | -- 加一个-字符, 这样可以启用多行注释内的代码 |
获得脚本参数
1 | print(arg[-1]) |
执行lua main.lua 1 2
打印出lua
, main.lua
, 1
和2
。
类型
lua类型是动态的, 每个标识符都携带了它自身的类型信息, 标识符可以赋不同类型的值。下面是全部类型:
- nil
- boolean: true or false
- number: 双精度浮点数, lua没有整数类型, 支持科学计数比如
a = 5e12
,b = 5e-3
- string: 可以用单引号, 双引号,
[[]]
包裹。它不应该被认为是字符串, 应该被认为是字节序列才对。和java很类似, 字符串是”只读”的 - function: 函数
- userdata: 自定义类型
- thread: 线程
- table: 表, 可以用来做list或哈希表
1 | print(type("Hello world")) --> string |
关于number的误解
有人认为lua应该支持整数类型, 那其实是不必要的, 只要用双精度的变量存放整数, 就和整数没有区别。
变量和作用域
基本概念:
- 使用没有创建的变量读取后为nil, 也可以对创建的变量写为nil来体现”删除这个变量”的意图, 可以认为读取为nil的变量就是不存在的变量
- 变量分为全局变量, 局部变量
- 变量的类型是动态类型, 每个标识符都存储了自身的类型
实验, 读取不存在的变量返回nil:
1 | print(not_found) |
局部变量必须用local
显示指定:
1 | a = 1 --- 全局变量 |
对局部变量, 与python不同, 变量的作用域与c类似, 块内部声明的变量块外无法访问, 且块内的局部变量声明能覆盖块外面的(这点很好, python的作用域太烂了):
1 | function fun() |
词法作用域:
1 | val = 1 |
这里的答案是1, 在函数定义期间, 内部变量的作用域已经确定了, foo内的val被认定是全局变量。
闭包的作用域:
这点很复杂, 放在后面讨论。
支持像go一样的多值赋值, 先解析右边, 然后依次赋值给左边, 这样可以做到不引入第三方变量而做交换操作:
1 | x, y = y, x |
疑问, 最外层的local有什么意义?(TODO)
交互式模式下的作用域:
在交互式模式下, 会为每行完整的指令生成一个程序块, 下面是一个很常见的”bug”:
1 | $ local i = 1 |
要解决这个问题, 就别使用完整的指令, 可以用do-end
自己构造一个程序块, 直到此程序块结束时, 才一次性执行里面的命令:
1 | > do local i = 1 |
直接屏蔽掉外层的local变量, 来避免冲突
1 | local a = 1 |
运算符
- +: 加
- -: 减 或 取负
- *: 乘
- /: 除法
- %: 取模
- ^: 次方
- //: 整除后向下取整数
- and: 且
- or: 或
- not: 否
- ..: 字符串连接, 值得一提的是number也能做字符串连接, 但需要前面的元素一定是字符串, 比如
10.."hello"
不合法 - #: 返回字符串或者table的长度, 对字符串是字节数目
1 | print("hello"..10) |
1 | print(#"你好world") |
if条件判断
在if中nil, false被规定为”假”, 其它都视为真。为了代码的严谨性, 别依赖这些假设, 统统用true/false就完了。
1 | if(1.1) |
循环结构
while:
1 | a = 1 |
for:
1 | for i=10,1,-1 do |
含义, 从10变到1(包含1),-1为步长(lua的语法题太甜了)。注意, 此处i其实被声明成了一个局部变量, 作用域在do内部。
1 | for i=1,10 |
因为默认步长为1。
repeat until:
1 | i = 1 |
循环控制:
可以使用break来结束一个循环, lua规定break是一个块的最后一个语句。有时候需要做调试, 在块的中间插入一个break, 可以这样做。
1 | ...一些代码 |
泛型for, 通过迭代器来遍历所有值:
1 | a = {1,2,3,4,5,6} |
ipairs是lua基础库提供的函数, 每次循环, i被赋予为键, v被赋予为值。对table可以使用下面的迭代器:
- ipairs: 迭代数组类型table的key和value, 仅限键为数字的
- pairs: 迭代table的key和value, 键值对
table用法1: 实现map
表是把索引映射到值的”关联数组”类型。标识符包含了table类型和它的一个指针, 也就是说它是一个引用类型。要创建一个brand new的table, 需要用构造表达式:
1 | a = {} |
table常常用来做映射, 键可以为非nil的数据类型, 值可以是所有类型, 如果键不存在, 那么取到的值为nil:
1 | -- 用来表示用户名代表的余额: |
下面进行一个实现, 看看将某个键对应的值设置为nil, 能不能起到删除的效果:
1 | a = {} |
因此根据lua的设计哲学, nil即不存在, 那么可以认为值为nil则对应键不存在。
一种神奇的语法糖:
1 | a["x"] = 10 |
这种语法糖专用于键为string, a["x"]
此时等价于a.x
。
table用法2: 数组
只需要把整数作为key来使用table就行了。下面的代码读取10行内容存储到table中, 并将其打印出来:
1 | a = {} |
在lua的习惯中, 数组通常以1作为索引起始值。对于#
运算符, 用于返回数组的长度, 为了看清它的逻辑, 下面演示一种空洞的情况:
1 | a = {} |
#
对table的原理就是, 从1开始计数, 一直数到nil, 中间到底有多少个元素。因此这里的返回结果为2。
如果要得到具有空洞的数组最大的索引值, 可以用下面的方法(table.maxn已经被移除, 我们自己实现个一样的):
1 | function table_maxn(t) |
table构造式的多种用法
三种构造法:
1 | -- days[1] = "Sunday"... |
牛逼应用, 做一个链表, 按输入相反的次序存储:
1 | list = nil |
混合构造:
1 | polyline = { |
string: 字符串(更准确的说是字节集合)
用单引号/双引号/[n个等号[]n个等号]
包裹形成字符串:
1 | a = 'Hello World' |
当字符串和number进行运算时, 字符串会尝试自动转化为number:
1 | print("12345"+1) |
..
运算符, 可以连接字符串:
1 | print("Hello".." World") |
#
运算符被用作获得字符串的字节长度:
1 | print(#"Hello") |
string
包提供了很多方法来操作字符串。其中很多方法都是面向英语编程即可, 这里提几个特别的。
字符串替换:
1 | ---@param s string |
1 | a = "hello world" |
s是被操作的字符串。pattern是原字符串, repl是替换字符串, n可选代表替换最多次数, 如果忽略则全部进行替换。
获取字符串的某些字节:
1 | ---@param s string |
1 | a = "hello world" |
s是被操作的字符串, i是开始索引(以1开始), j是结束索引(包含它)。返回值被标记为integer ...
, 在”函数”中讨论这个。
函数
简单入门:
1 | function add(a, b) |
函数的参数是局部, 也就是local的, 这点需要注意。
当参数只有一个且为字符串字面量或table构造式时, 括号可有可无:
1 | > print "Hello World" |
面向对象的特殊调用: 冒号运算符
TODO: 后面介绍
参数传递多或少的问题:
1 | function f(a,b) return a or b end |
这里的行为和赋值的参数多或少一个意思, 比如:
1 | a = 10, 20 -- 20被丢弃 |
无返回值, 等价于返回nil:
1 | function foo() |
多返回值:
1 | function swap(a, b) |
另外一个例子:
1 | function foo1() |
奇葩情况:
1 | -- 奇葩情况1 |
对于第一种奇葩情况保持规避态度, 第二种是因为多返回值函数参与运算时, 返回值个数会被调整为1。
table构造式中的多返回值:
1 | function foo2() |
unpace函数(TODO:为什么这个函数被遗弃了?):
1 | a, b = unpack{10,20} |
unpack仅仅接收一个参数, 将数组”解包”, 因为参数类型为table字面值, 所以这里可以省略括号。
变长参数:
...
表示函数可以接受不同数量的实参, 代表0个或多个参数的集合。在函数中, 如果需要使用变长参数, 需要用{...}
生成数组, 看下面的例子:
1 | function sum(...) |
...
仅仅代表表达式, 可以理解为多个参数用逗号分隔。要拿出可变参数数组, 还得用{...}
分隔。
可以认为有这样的关系, 即: unpack({...}) 等价于 ...
。
需要传参时, ...
也可以指代所有的参数集合:
1 | function printEachForLine(...) |
我们可以利用可变参数实现unpack的逆向操作pack:
1 | function pack(...) |
这个操作没啥意义, 就是写着玩的。因为{v1, v2, v3}
就是pack操作, 不比这个简单?
如果需要传入几个固定参数+可变参数, 那么这么写:
1 | function func(arg1, arg2, ...) |
下面我们来实现printf:
1 | function printf(fmt, ...) |
select函数专门用来判断可变参数的个数:
1 | function test(...) |
那么select和#
有什么区别呢:
1 | function test(...) |
这里的区别不言而喻, #
运算符会拿取table中最大的数字索引值作为参数。
而select会拿取所有, 管它是否为nil, 这就像开外挂一样嘛?(TODO: 这里的select和#包括ipairs,pairs是怎么实现的?)
1 | a = {["a"]="test"} |
传参的较好实践:
不要忘记对于table构造式和string字面量的函数调用语法糖, 因此很容易可以实现这样的一个构造:
1 | w = Window{x = 0, y = 0, width = 300, height = 200} |
这是一种很好的设计, 具有较高的可读性。
深入函数
一种基本的语法糖:
1 | function foo(x) return 2*x end |
可以认为function函数声明就是一个构造式, 这种无名的构造可以认为是匿名函数。下面利用了匿名函数来实现排序:
1 | a = {1,2,3,2,3,4,56,1,-2,3,-8} |
匿名函数的高级应用, 求导:
1 | function derivative(f, delta) |
此处的原理就是f在x的导数就是(f(x+delta)-f(x))/delta
, 其中delta趋近于0。
这里利用的技巧就是, delta
传入的值为nil时, delta
为1e-4, 这个技巧很常用。
高级的东西, 函数闭包:
将一个函数写在另一个函数之内, 这个内部的函数能访问到外部函数的局部变量, 这叫做”词法域”, 下面是一个有用的例子:
1 | -- 名字列表 |
实现一个计数器:
1 | function newCounter() |
库函数的基本实现:
1 | Lib = {} |
局部函数及其语法糖:
1 | local f = function f() |
递归的局部函数:
1 | -- 错误, 闭包里面的fact其实代指的是全局函数 |
语法糖的拓展:
1 | -- 合法的 |
尾调用消除:
TODO: 这个不影响编程, 是一个优化, 先搁置了。
迭代器和泛型for
lua中, 迭代器就是一个函数, 每调用一次, 返回集合中下一个元素。迭代器的实现依赖”闭包”的特性。
一个简单的图景, 数值产生器:
1 | function values() |
这个例子和前面闭包的例子很像, 不必多说。
与泛型for结合:
1 | function values(t) |
values()
返回一个函数, 每次循环element都调用这个函数取得一个数值, 如果数值为nil, 那么就结束循环。
更好的例子, 扫描文件的所有单词:
1 | function allwords() |
泛型for的原理:
一个泛型for包含三个概念, 一个迭代器函数, 一个恒定状态, 一个控制变量, 下面通过例子说明:
1 | a = {"one", "two", "three"} |
ipairs(a)
返回三个东西, 第一个为迭代器函数, 第二个是控制变量初值, 第三个为恒定状态初值。这三个东西被for循环保存, 每次循环for都会调用迭代器函数, 返回上一次的控制变量和恒定状态。下面是基本用法和原理:
1 | for <var-list> in <exp-list> do |
通常<exp-list>
只包含一个元素, 多个元素用逗号分割, 这里不讨论多个元素的情况。
变量列表可以有多个变量, 其中第一个元素为控制变量, 在循环过程中决不会为nil, 当它为nil时, 循环结束, 这也是”控制”说法的由来。
等价形式:
1 | for var_1, ... var_n in <explist> do <block> end |
下面展示一种较为奇葩的情况:
1 | a = 0 |
如果
实现ipairs:
1 | function myipairs(t) |
实现pairs:
1 | function mypairs(tbl) |
pairs迭代依赖了next, 这个可以拿到下一个键名, 这个是黑科技?(TODO: 怎么做next?)
下面进行一个实现, 证明对设置某个对应的键的值为nil, 就相当于删除了这个键值对, next都拿不到啊:
1 | function mySelectLength(t) |
那么select('#',...)
是咋实现的呢?下面给个思路:
1 | function mySelectReally(...) |
编译执行和错误
TODO
协程
关于协程的函数放在名为”coroutine”的table中(别忘了lua靠table实现模块)。create用来创建一个协程:
1 | co = coroutine.create(function () print("hi") end) |
协程的四种状态:
- 挂起: suspended
- 运行: running
- 死亡: dead
- 正常: normal
通过coroutine.status(co)
来检查协程的状态:
1 | print(coroutine.status(co)) --> suspended |
函数coroutine.resume(co)
用于启动一个协程的执行, 此时协程的状态变成运行。
1 | coroutine.resume(co) --> hi |
打印后, 协程有运行状态变为死亡状态:
1 | print(coroutine.status(co)) --> dead |
协程允许主动让出执行权, 下面是一个全面的例子:
1 | co = coroutine.create(function () |
resume-yield的消息传递机制:
可以通过一对resume-yield来交换数据, 特别的是, 第一次调用resume时, 没有yield在等待它, 那么数据被放入协程函数的参数列表中, 下面是一个例子:
1 | co = coroutine.create(function (a,b,c) |
一个更好的例子, 用来做数值产生器:
1 | co = coroutine.create(function (...) |
resume的返回值中, 第一个值为true表示没有错误, 后面都是yield返回的对象。
当一个协程结束时, 它的函数返回值作为对应resume的返回值。
lua协程的概述:
与go的不同, lua的协程被称为”非对称的协同程序”。它是非抢占式的, 协程必须自发挂起。lua的协程在任意时刻只有一个协程在运行, 我认为这就无疑像单线程了, 因为一个协程启动了另外一个协程, 自己此时就被阻塞在那了, 只是换了另外一个协程来执行, 整体来说还是单个协程在执行。
如果要多线程下载文件, 简述下面的方法:
1 | function download(host, file) |
这里的技巧是, 将IO设置为非阻塞的, 然后没有事件就立马返回。把时间留给有IO的协程。
TODO: 所以我觉得这就像随时切换执行流的单线程程序, 我的认知有误吗?