《lua程序设计》学习记录

前言

为了配合redis做原子操作学习lua, 正在读《Lua程序设计》, 此书短小精悍, 应该几日就能读完, 记下这篇笔记便于回忆。正在持续施工中…

A Very Beginning: Hello World

1
2
3
4
5
6
7
8
9
print("Hello World")

-- 行注释

--[[
多行注释
]]

-- output: Hello World

学习编程语言的开始, 感觉回到了python。lua用--注释行, 用--[[...]]--注释多行。

一个简洁的例子: fact

下面简要演示了lua语言的语法图景, 有个简单的印象:

1
2
3
4
5
6
7
8
9
10
11
function fact(n)
if n == 0 then
return 1
else
return n * fact(n-1)
end
end

print("enter a number: ")
a = io.read("*number")
print(fact(a))

词法规范的特殊规则

  • 避免用一个下划线跟着一个或多个大写字母作为标识符, 因为这类标识符有特殊的用途。

在命令行里打lua, 然后就进入了lua的交互式模式, 此时输入_PROMPT = '$ ', 这样你的lua交互模式的命令提示符就变成$ 了, 这是特殊用途之一。

还有其它的特殊用途我现在还不知道(TODO)。

  • 不用一个下划线作为标识符, 一个下划线作为”哑变量”使用。哑变量是什么我现在不知道(TODO)。

解除多行注释

下面介绍了一个奇技淫巧:

1
2
3
4
5
-- 加一个-字符, 这样可以启用多行注释内的代码

---[[
print(10)
--]]

获得脚本参数

1
2
3
4
print(arg[-1])
print(arg[0])
print(arg[1])
print(arg[2])

执行lua main.lua 1 2打印出lua, main.lua, 12

类型

lua类型是动态的, 每个标识符都携带了它自身的类型信息, 标识符可以赋不同类型的值。下面是全部类型:

  • nil
  • boolean: true or false
  • number: 双精度浮点数, lua没有整数类型, 支持科学计数比如a = 5e12, b = 5e-3
  • string: 可以用单引号, 双引号, [[]]包裹。它不应该被认为是字符串, 应该被认为是字节序列才对。和java很类似, 字符串是”只读”的
  • function: 函数
  • userdata: 自定义类型
  • thread: 线程
  • table: 表, 可以用来做list或哈希表
1
2
3
4
5
6
7
print(type("Hello world"))      --> string
print(type(10.4*3)) --> number
print(type(print)) --> function
print(type(type)) --> function
print(type(true)) --> boolean
print(type(nil)) --> nil
print(type(type(X))) --> string, 表明type(X)的返回值是string

关于number的误解

有人认为lua应该支持整数类型, 那其实是不必要的, 只要用双精度的变量存放整数, 就和整数没有区别。

变量和作用域

基本概念:

  • 使用没有创建的变量读取后为nil, 也可以对创建的变量写为nil来体现”删除这个变量”的意图, 可以认为读取为nil的变量就是不存在的变量
  • 变量分为全局变量, 局部变量
  • 变量的类型是动态类型, 每个标识符都存储了自身的类型

实验, 读取不存在的变量返回nil:

1
2
3
print(not_found)

--- output: nil

局部变量必须用local显示指定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
a = 1                   --- 全局变量

function func1()
b = 10 --- 全局变量
local a = 11 --- 局部变量
end

function func2()
b = 20 --- 全局变量
a = 111 --- 全局变量
end

print(a, b)
func1()
print(a, b) --- 调用这个函数, 全局变量就被声明了, 可见全局变量自由度很高
func2()
print(a, b)

--- output
--- 1 nil
--- 1 10
--- 111 20

对局部变量, 与python不同, 变量的作用域与c类似, 块内部声明的变量块外无法访问, 且块内的局部变量声明能覆盖块外面的(这点很好, python的作用域太烂了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function  fun()
local a = 1
if a == 1
then
-- 与外部的a无关, 这个词法块有自己的a
local a = 2
local b = 3
end

print(a) -- 1
print(b) -- nil 超出块作用域
end

fun()

词法作用域:

1
2
3
4
5
6
7
8
9
10
11
12
val = 1

function foo()
print(val)
end

function bar()
local val = 2
foo()
end

bar()

这里的答案是1, 在函数定义期间, 内部变量的作用域已经确定了, foo内的val被认定是全局变量。

闭包的作用域:

这点很复杂, 放在后面讨论。

支持像go一样的多值赋值, 先解析右边, 然后依次赋值给左边, 这样可以做到不引入第三方变量而做交换操作:

1
x, y = y, x

疑问, 最外层的local有什么意义?(TODO)

交互式模式下的作用域:

在交互式模式下, 会为每行完整的指令生成一个程序块, 下面是一个很常见的”bug”:

1
2
3
$ local i = 1
$ print(i)
nil

要解决这个问题, 就别使用完整的指令, 可以用do-end自己构造一个程序块, 直到此程序块结束时, 才一次性执行里面的命令:

1
2
3
4
5
> do local i = 1
>> print(i)
>> end
1
>

直接屏蔽掉外层的local变量, 来避免冲突

1
2
3
4
5
local a = 1
do
local a -- 隐式的local a = nil
-- do something with a...
end

运算符

  • +: 加
  • -: 减 或 取负
  • *: 乘
  • /: 除法
  • %: 取模
  • ^: 次方
  • //: 整除后向下取整数
  • and: 且
  • or: 或
  • not: 否
  • ..: 字符串连接, 值得一提的是number也能做字符串连接, 但需要前面的元素一定是字符串, 比如10.."hello"不合法
  • #: 返回字符串或者table的长度, 对字符串是字节数目
1
2
3
print("hello"..10)

--- output: hello10
1
print(#"你好world")

if条件判断

在if中nil, false被规定为”假”, 其它都视为真。为了代码的严谨性, 别依赖这些假设, 统统用true/false就完了。

1
2
3
4
5
6
7
8
9
10
11
12
13
if(1.1)
then
print("ok")
end

if("我")
then
print("ok")
end

--- output:
--- ok
--- ok

循环结构

while:

1
2
3
4
5
6
7
8
a = 1
while(a <= 10)
do
print(a)
a = a + 1
end

--- output: 打印1-10

for:

1
2
3
4
5
for i=10,1,-1 do
print(i)
end

--- output: 打印10-1

含义, 从10变到1(包含1),-1为步长(lua的语法题太甜了)。注意, 此处i其实被声明成了一个局部变量, 作用域在do内部。

1
2
3
4
5
6
for i=1,10
do
print(i)
end

--- output: 打印1-10

因为默认步长为1。

repeat until:

1
2
3
4
5
6
7
i = 1
repeat
print(i)
i = i + 1
until (i == 11)

--- output: 打印1-11

循环控制:

可以使用break来结束一个循环, lua规定break是一个块的最后一个语句。有时候需要做调试, 在块的中间插入一个break, 可以这样做。

1
2
3
...一些代码
do break end
...一些代码

泛型for, 通过迭代器来遍历所有值:

1
2
3
4
5
6
7
8
9
10
11
12
13
a = {1,2,3,4,5,6}
for i,v in ipairs(a) do
print(i, v)
end

--[[ output:
1 1
2 2
3 3
4 4
5 5
6 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 用来表示用户名代表的余额:
a = {}
a["Jack"] = 1000
a["Mary"] = 2000

-- 验证"引用类型"的论断
b = a
b["Mark"] = 3000

print(a["Jack"])
print(a["Mary"])
print(a["Mark"])

--[[ output:
1000
2000
3000
--]]

下面进行一个实现, 看看将某个键对应的值设置为nil, 能不能起到删除的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a = {}
a["x"] = 10
a["x"] = nil

for k,v in pairs(a) do
print(k,v)
end

print(#a)
print(select('#', ...))

--[[ output:
0
0
--]]

因此根据lua的设计哲学, nil即不存在, 那么可以认为值为nil则对应键不存在。

一种神奇的语法糖:

1
2
a["x"] = 10
print(a.x)

这种语法糖专用于键为string, a["x"]此时等价于a.x

table用法2: 数组

只需要把整数作为key来使用table就行了。下面的代码读取10行内容存储到table中, 并将其打印出来:

1
2
3
4
5
6
7
8
a = {}
for i = 1, 10, 1 do
a[i] = io.read()
end

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

在lua的习惯中, 数组通常以1作为索引起始值。对于#运算符, 用于返回数组的长度, 为了看清它的逻辑, 下面演示一种空洞的情况:

1
2
3
4
5
6
7
8
a = {}
a[-2] = 0
a[1] = 10
a[2] = 20
a[10] = 30
print(#a)

-- output: 2

#对table的原理就是, 从1开始计数, 一直数到nil, 中间到底有多少个元素。因此这里的返回结果为2。

如果要得到具有空洞的数组最大的索引值, 可以用下面的方法(table.maxn已经被移除, 我们自己实现个一样的):

1
2
3
4
5
6
7
8
9
10
11
12
function table_maxn(t)
local mn=nil;
for k, v in pairs(t) do
if(mn==nil) then
mn=v
end
if mn < v then
mn = v
end
end
return mn
end

table构造式的多种用法

三种构造法:

1
2
3
4
5
6
7
8
9
10
11
12
13
-- days[1] = "Sunday"...
days = {"Sunday", "Monday", "Tuesday"}

-- salary.Jack = 1000...
salary = {Jack=1000, Mark=2000}

-- opcodes["+"] = "add"...
opcodes = {
["+"] = "add",
["-"] = "sub",
["*"] = "mul",
["/"] = "div"
}

牛逼应用, 做一个链表, 按输入相反的次序存储:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
list = nil
for line in io.lines() do
list = {next=list,value=line}
end

l = list
while l do
print(l.value)
l = l.next
end

--[[ input:
1
2
3
--]]

--[[ output:
3
2
1
--]]

混合构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
polyline = {
color="blue",
thickness=2,
npoints=4,
{x=0, y=0},
{x=-10, y=0},
{x=-10, y=1},
{x=0, y=1}
}

print(polyline.color)
print(polyline[2].x, polyline[2].y)

--[[ output:
blue
10 0
--]]

string: 字符串(更准确的说是字节集合)

用单引号/双引号/[n个等号[]n个等号]包裹形成字符串:

1
2
3
4
5
6
a = 'Hello World'
a = "Hello World"
a = [[Hello World]]

-- 下面这种方法是为了解决某种冲突, 和markdown代码块里面包含```如出一辙
a = [===[Hello World]===]

当字符串和number进行运算时, 字符串会尝试自动转化为number:

1
2
3
print("12345"+1)

-- output: 12346

..运算符, 可以连接字符串:

1
2
3
4
5
6
7
8
9
print("Hello".." World")
print(10 .. 20)
print(10 .. "Hello")

--[[ output:
Hello World
1020
10Hello
--]]

#运算符被用作获得字符串的字节长度:

1
2
3
print(#"Hello")

-- output: 5

string包提供了很多方法来操作字符串。其中很多方法都是面向英语编程即可, 这里提几个特别的。

字符串替换:

1
2
3
4
5
6
7
8
---@param s       string
---@param pattern string
---@param repl string|number|table|function
---@param n? integer
---@return string
---@return integer count
---@nodiscard
function string.gsub(s, pattern, repl, n)
1
2
3
4
5
6
7
8
9
a = "hello world"
b = string.gsub(a, "world", "markity")
print(a)
print(b)

--[[ output:
hello world
hello markity
--]]

s是被操作的字符串。pattern是原字符串, repl是替换字符串, n可选代表替换最多次数, 如果忽略则全部进行替换。

获取字符串的某些字节:

1
2
3
4
5
6
---@param s  string
---@param i? integer
---@param j? integer
---@return integer ...
---@nodiscard
function string.byte(s, i, j) end
1
2
3
4
5
a = "hello world"
result = {string.byte(a, 1, 5)}
print(result[1], result[2])

-- output: 104 101

s是被操作的字符串, i是开始索引(以1开始), j是结束索引(包含它)。返回值被标记为integer ..., 在”函数”中讨论这个。

函数

简单入门:

1
2
3
4
5
6
7
function add(a, b)
return a+b
end

print(add(10, 20))

-- output: 30

函数的参数是局部, 也就是local的, 这点需要注意。

当参数只有一个且为字符串字面量或table构造式时, 括号可有可无:

1
2
3
4
> print "Hello World"
Hello World
> type {}
table

面向对象的特殊调用: 冒号运算符

TODO: 后面介绍

参数传递多或少的问题:

1
2
3
4
5
function f(a,b) return a or b end

f(3) -- a=3, b=nil
f(3,4) -- a=3, b=4
f(3,4,5)-- a=3, b=4, 5被丢弃了

这里的行为和赋值的参数多或少一个意思, 比如:

1
2
a = 10, 20 -- 20被丢弃
a, b, c = 0 -- a = 0, b = nil, c = nil

无返回值, 等价于返回nil:

1
2
3
4
function foo()
end

x = foo() -- x = nil

多返回值:

1
2
3
4
5
6
7
function swap(a, b)
return b, a
end

print(swap(10, 20))

-- output: 20 10

另外一个例子:

1
2
3
4
5
6
7
8
9
10
11
function foo1()
return "a"
end

function foo2()
return "b", "c"
end

x,y,z = foo1(), foo2()

-- x="a,", y="b", z="c"

奇葩情况:

1
2
3
4
5
6
7
8
9
10
11
-- 奇葩情况1
function foo2()
return "a", "b"
end
x, y = foo2(), 20
print(x, y)
-- output: a 20

-- 奇葩情况2
print(foo2() .. 20)
-- output: a20

对于第一种奇葩情况保持规避态度, 第二种是因为多返回值函数参与运算时, 返回值个数会被调整为1。

table构造式中的多返回值:

1
2
3
4
function foo2()
return "a", "b"
end
a = {foo2()} -- a[1]="a", a[2]="b"

unpace函数(TODO:为什么这个函数被遗弃了?):

1
a, b = unpack{10,20}

unpack仅仅接收一个参数, 将数组”解包”, 因为参数类型为table字面值, 所以这里可以省略括号。

变长参数:

...表示函数可以接受不同数量的实参, 代表0个或多个参数的集合。在函数中, 如果需要使用变长参数, 需要用{...}生成数组, 看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function sum(...)
print("传入"..#{...}.."个数字")
local result = 0
for i,v in ipairs({...}) do
result = result + v
end

return result
end

print(sum(1,2,3))

-- output:
-- 传入3个数字
-- 6

...仅仅代表表达式, 可以理解为多个参数用逗号分隔。要拿出可变参数数组, 还得用{...}分隔。

可以认为有这样的关系, 即: unpack({...}) 等价于 ...

需要传参时, ...也可以指代所有的参数集合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function printEachForLine(...)
-- 别忘了这个语法糖, func({...}) == func{...}
for k, v in ipairs{...} do
print(v)
end
end

printEachForLine(1, 2, 3)

--[[ output:
1
2
3
]]

我们可以利用可变参数实现unpack的逆向操作pack:

1
2
3
function pack(...)
return {...}
end

这个操作没啥意义, 就是写着玩的。因为{v1, v2, v3}就是pack操作, 不比这个简单?

如果需要传入几个固定参数+可变参数, 那么这么写:

1
2
function func(arg1, arg2, ...)
end

下面我们来实现printf:

1
2
3
4
5
function printf(fmt, ...)
return io.write(string.format(fmt, ...))
end

printf("Hello %s\n", "Markity")

select函数专门用来判断可变参数的个数:

1
2
3
4
5
6
7
8
9
10
11
12
13
function test(...)
print(select('#', ...))
end

test(1,2,3)
test(1,2,nil)
test(1,2,nil,4)

--[[ output:
3
3
4
--]]

那么select和#有什么区别呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
function test(...)
print(#{...})
end

test(1,2,3)
test(1,2,nil)
test(1,2,nil,4)

--[[ output:
3
2
4
]]

这里的区别不言而喻, #运算符会拿取table中最大的数字索引值作为参数。

而select会拿取所有, 管它是否为nil, 这就像开外挂一样嘛?(TODO: 这里的select和#包括ipairs,pairs是怎么实现的?)

1
2
3
4
5
a = {["a"]="test"}

for k,v in ipairs(a) do
print(k,v)
end

传参的较好实践:

不要忘记对于table构造式和string字面量的函数调用语法糖, 因此很容易可以实现这样的一个构造:

1
w = Window{x = 0, y = 0, width = 300, height = 200}

这是一种很好的设计, 具有较高的可读性。

深入函数

一种基本的语法糖:

1
2
3
4
5
function foo(x) return 2*x end

-- 等价写法

foo = function(x) return 2*x end

可以认为function函数声明就是一个构造式, 这种无名的构造可以认为是匿名函数。下面利用了匿名函数来实现排序:

1
2
3
4
5
6
7
a = {1,2,3,2,3,4,56,1,-2,3,-8}
table.sort(a, function(a,b) return (a>b) end)
for k,v in ipairs(a) do
print(v)
end

-- output: 排序结果

匿名函数的高级应用, 求导:

1
2
3
4
5
6
7
8
9
10
11
12
function derivative(f, delta)
delta = delta or 1e-4
return function(x)
return (f(x + delta)-f(x))/delta
end
end

c = derivative(math.sin)

print(math.cos(10), c(10))

-- output: -0.83907152907645 -0.83904432662041

此处的原理就是f在x的导数就是(f(x+delta)-f(x))/delta, 其中delta趋近于0。

这里利用的技巧就是, delta传入的值为nil时, delta为1e-4, 这个技巧很常用。

高级的东西, 函数闭包:

将一个函数写在另一个函数之内, 这个内部的函数能访问到外部函数的局部变量, 这叫做”词法域”, 下面是一个有用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 名字列表
names = {"Peter", "Paul", "Mary"}

-- 年级列表
grades = {Mary = 10, Paul = 7, Peter = 8}

function sortbygrade(names, grades)
table.sort(names, function(n1, n2)
return grades[n1] > grades[n2]
end )
end

sortbygrade(names, grades)

-- 年级大的排在前面
print(names[1], names[2], names[3])

实现一个计数器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function newCounter()
local i = 0
return function()
i = i + 1
return i
end
end

counter = newCounter()
print(counter())
print(counter())
print(counter())

--[[ output:
1
2
3
--]]

库函数的基本实现:

1
2
3
4
5
6
7
Lib = {}

Lib.add = function(x,y) return x+y end
Lib.sub = function(x,y) return x-y end

-- 此外还有语法糖
function Lib.multipy(x,y) return x*y end

局部函数及其语法糖:

1
2
3
4
5
local f = function f()

-- 下面是等价形式
local function f()
end

递归的局部函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
-- 错误, 闭包里面的fact其实代指的是全局函数
do
local fact = function(n)
if n == 0 then
return 1
else
return n*fact(n-1)
end
end
end

-- 要解决这个问题可以这么做
do
local fact
fact = function(n)
if n == 0 then
return 1
else
return n*fact(n-1)
end
end
end

-- 对于间接进行的递归, 还是必须使用前向声明
do
local f,g
function g()
...一些代码 f() 一些代码...
end

-- 别忘了function f()是 f = function()的语法糖
function f()
...一些代码 g() 一些代码...
end
end

语法糖的拓展:

1
2
3
4
5
6
7
8
9
10
11
12
-- 合法的
local function fact(n)
if n == 0 then
return 1
else
return n*fact(n-1)
end
end

-- 因为上面的代码被拓展为
local fact
fact = ... -- 此处省略

尾调用消除:

TODO: 这个不影响编程, 是一个优化, 先搁置了。

迭代器和泛型for

lua中, 迭代器就是一个函数, 每调用一次, 返回集合中下一个元素。迭代器的实现依赖”闭包”的特性。

一个简单的图景, 数值产生器:

1
2
3
4
5
6
7
8
9
10
11
12
function values()
local i = 0
return function() i = i+1; return i end
end

iter = values()
while true do
local element = iter()
print(element)
end

-- output: 从1一直递增

这个例子和前面闭包的例子很像, 不必多说。

与泛型for结合:

1
2
3
4
5
6
7
8
function values(t)
local i = 0
return function() i = i+1; return t[i] end
end

for element in values({1,2,3}) do
print(element)
end

values()返回一个函数, 每次循环element都调用这个函数取得一个数值, 如果数值为nil, 那么就结束循环。

更好的例子, 扫描文件的所有单词:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function  allwords()
local line = io.read()
local pos = 1
return function ()
while line do
local s, e = string.find(line, "%w+", pos)
if s then
pos = e + 1
return string.sub(line, s, e)
else
line = io.read()
pos = 1
end
end
return nil
end
end

for word in allwords() do
print(word)
end

-- 要运行这个例子用重定向: lua main.lua < file

-- output: 一个单词一行

泛型for的原理:

一个泛型for包含三个概念, 一个迭代器函数, 一个恒定状态, 一个控制变量, 下面通过例子说明:

1
2
3
4
a = {"one", "two", "three"}
for i, v in ipairs(a) do
print(i, v)
end

ipairs(a)返回三个东西, 第一个为迭代器函数, 第二个是控制变量初值, 第三个为恒定状态初值。这三个东西被for循环保存, 每次循环for都会调用迭代器函数, 返回上一次的控制变量和恒定状态。下面是基本用法和原理:

1
2
3
for <var-list> in <exp-list> do
<body>
end

通常<exp-list>只包含一个元素, 多个元素用逗号分割, 这里不讨论多个元素的情况。

变量列表可以有多个变量, 其中第一个元素为控制变量, 在循环过程中决不会为nil, 当它为nil时, 循环结束, 这也是”控制”说法的由来。

等价形式:

1
2
3
4
5
6
7
8
9
10
11
12
for var_1, ... var_n in <explist> do <block> end

-- 等价于
do
local _f, _s, _var = <explist>
while true do
local var_1, ..., var_n = _f(_s, _var)
_var = var_1
if _var == nil then break end
<block>
end
end

下面展示一种较为奇葩的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
a = 0

function test()
a = a + 1
if a == 6 then
return nil
end
return a
end

for element in test do
print(element)
end

--[[ output:
1
2
3
4
5
--]]

如果里面少于三个元素, 那么其余补nil, 因此第一个循环时, element = 1, 第二次循环时, 调用test(nil, nil), 拿到element = 2, 以此类推。

实现ipairs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function myipairs(t)
local i = 0
return function ()
i = i + 1
if t[i] == nil then return nil end
return i, t[i]
end
end

a = {1, 2, 3, 4, 5}

for index, value in myipairs(a) do
print(index, value)
end

实现pairs:

1
2
3
4
5
6
7
8
9
10
11
12
function mypairs(tbl)
return function(state, control) -- iterator
control = next(tbl, control)
local value = tbl[control]
return control, value
end, nil, nil, nil
end

local tb = {aa = "bb", 3, 4, 5}
for key, value in mypairs(tb) do
print(key, value)
end

pairs迭代依赖了next, 这个可以拿到下一个键名, 这个是黑科技?(TODO: 怎么做next?)

下面进行一个实现, 证明对设置某个对应的键的值为nil, 就相当于删除了这个键值对, next都拿不到啊:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function mySelectLength(t)
local ret = 0
local last = nil
while true do
last = next(t, last)
if last == nil then
return ret
end
ret = ret + 1
end
end

function myJingHao(t)
local idx = 0
while true do
if t[idx+1] == nil then
return idx
end
idx = idx + 1
end
end

function testSelect(...)
return select('#', ...)
end

-- output: 7, 6, 3
print(testSelect(1,2,3,nil,4,5,6))
print(mySelectLength({1,2,3,nil,4,5,6}))
print(myJingHao({1,2,3,nil,4,5,6}))

那么select('#',...)是咋实现的呢?下面给个思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
function mySelectReally(...)
local t = 0
for k, v in pairs({...}) do
if type(v) == "number" and v > t then
t = k
end
end
return t
end

print(mySelectReally(1,2,3,nil,4,5,6))

-- output: 7

编译执行和错误

TODO

协程

关于协程的函数放在名为”coroutine”的table中(别忘了lua靠table实现模块)。create用来创建一个协程:

1
2
3
co = coroutine.create(function () print("hi") end)

print(co) --> thread: 0x...

协程的四种状态:

  • 挂起: 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
co = coroutine.create(function ()
for i = 1,10 do
print("co", i)
coroutine.yield()
end
end)

coroutine.resume(co)
coroutine.resume(co)
coroutine.resume(co)
coroutine.resume(co)

--[[
co 1
co 2
co 3
co 4
--]]

resume-yield的消息传递机制:

可以通过一对resume-yield来交换数据, 特别的是, 第一次调用resume时, 没有yield在等待它, 那么数据被放入协程函数的参数列表中, 下面是一个例子:

1
2
3
4
5
6
7
co = coroutine.create(function (a,b,c)
print(a, b, c)
end)

coroutine.resume(co, 1, 2, 3)

-- output: 1 2 3

一个更好的例子, 用来做数值产生器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
co = coroutine.create(function (...)
local next = {...}
local sum = 0
while true do
sum = 0
for k, v in pairs(next) do
sum = sum + v
end
next = {coroutine.yield(sum)}
end
end)

print(coroutine.resume(co, 5, 3, 2))
print(coroutine.resume(co, 5, 3, 1))

--[[ output:
true 10
true 9
--]]

resume的返回值中, 第一个值为true表示没有错误, 后面都是yield返回的对象。

当一个协程结束时, 它的函数返回值作为对应resume的返回值。

lua协程的概述:

与go的不同, lua的协程被称为”非对称的协同程序”。它是非抢占式的, 协程必须自发挂起。lua的协程在任意时刻只有一个协程在运行, 我认为这就无疑像单线程了, 因为一个协程启动了另外一个协程, 自己此时就被阻塞在那了, 只是换了另外一个协程来执行, 整体来说还是单个协程在执行。

如果要多线程下载文件, 简述下面的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function download(host, file)
local c = assert(socket.connect(host,80))
local cout = 0
c:send("GET " .. file .. " HTTP/1.0\r\n\r\n")
while true do
local s, status, partial = receive(c)
count = count + #(s or partial)
if status = "closed" then break end
end
c:close()
print(file, count)
end

function receive(connection)
connection:settimeout(0)
local s, status, partial = connection:receive(2^10)
if status == "timeout" then
coroutine.yield(connection)
end
return s or partial, status
end

这里的技巧是, 将IO设置为非阻塞的, 然后没有事件就立马返回。把时间留给有IO的协程。

TODO: 所以我觉得这就像随时切换执行流的单线程程序, 我的认知有误吗?