Programming in Lua 4th(不完全翻译) 第一章

开始

Hello World

第一个程序:

vi hello.lua

输入:

print("Hello World")

编译执行:

lua hello.lua

Chunks(块)

Chunk就是有多条语句的代码块。

Linux下键入lua即进入交互模式。在5.3版本以后我们在交互模式下不需要额外的键入 =等于号。

% lua
 Lua 5.3 Copyright (C) 1994-2016 Lua.org, PUC-Rio
 > math.pi / 4 --> 0.78539816339745
 > a = 15
 > a^2 --> 225   这里老版本的话是 = a ^ 2
 > a + 2 --> 17

在非交互模式下用print封装代码成块。

print(math.pi / 4)
a = 15
print(a^2)
print(a + 2)

为了便于调试,我们有时需要在执行一段lua文本以后进入交互模式:

lua -i hello.lua

结果:

[root@VM_0_6_centos lua]# lua -i hello.lua 
Lua 5.3.5  Copyright (C) 1994-2018 Lua.org, PUC-Rio
hello world
> 

另外用dofile可以在交互模式下引入lua文件:

[root@VM_0_6_centos lua]# lua
Lua 5.3.5  Copyright (C) 1994-2018 Lua.org, PUC-Rio
> dofile("hello.lua")
hello world
>

dofile有点类似于Java中的import, C中的#include

一些词法约定

名字定义和保留字

属性名可以是任意的英文字母,数字,下划线,但是不能以数字开头。

同时应该避免在下划线后接上一个或多个大写字母,例如_VERSION,这是保留字在某些特定的情况下使用。另外在Lua中还有这些保留字:

if else elseif 
for while then until goto break repeat do end in
and or not
true false
function return 
nil
local 

需要注意的是Lua也是大小写区分的。

注释

单行注释用--。多行注释像这样:

--[[
	print(10)  -- 这样这条语句就不会被执行
]]

注释代码的时候通常我们还会在后面的大括号前加多两杠:

--[[
	print(10)  -- 这样这条语句就不会被执行
--]]

lua的注释有一点和其他语言不太一样的就是在这里有个技巧,我们只要在注释开始前加多一杠就可以重新激活这段代码:

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

分隔符

单条语句行尾不用;分隔,用的话也没问题。

全局变量

变量不需要被提前声明,直接使用它,如果未被初始化不会报错而是得到一个nil值。

如果我们给一个变量分配nil值,Lua的行为就好像我们从来没有使用过这个变量。

赋值nil后,Lua可以选择回收这个变量。

类型和值

Lua是动态类型语言(弱类型语言)。每个值承载它的类型。

使用type()函数可以得到它们的类型,注意type()函数返回的是string

有8钟基础类型,分别是:nil, Boolean, number, string, userdata, function, thread, table

 > type(nil) --> nil
 > type(true) --> boolean
 > type(10.4 * 3) --> number
 > type("Hello world") --> string
 > type(io.stdin) --> userdata
 > type(print) --> function
 > type(type) --> function
 > type({}) --> table
 > type(type(X)) --> string

需要注意是的变量是弱类型变量,即可变:

> type(a) --> nil 
 > a = 10
 > type(a) --> number
 > a = "a string!!"
 > type(a) --> string
 > a = nil
 > type(a) --> nil

通常,当我们对不同类型使用单个变量时,结果是混乱的代码。但是,有时明智地使用这个工具是有帮助的,例如使用nil来区分正常的返回值和异常情况

Userdata类型

userdata类型允许任意C数据作为Lua的变量存储。

userdata类型在Lua中没有预定义的操作,除了赋值和等价测试。

userdata类型用于表示应用程序或用C编写的库所创建的新类型。例如,标准的IO操作。

Nil类型

nil是一种具有单个值的类型,它的主要性质是与任何其他值不同。Luanil作为一种非值,表示没有值。在前文中我们也提过, 全局变量在第一次赋值之前默认有一个nil值,我们可以给全局变量赋值为nil来删除它。

Boolean类型

Boolean类型有两个值:@false{} 和 @true{},代表的是传统的布尔值。

Lua中,falsenil代表的是false,而其他任何值代表的是true

和C区分,C中0, 空字符串代表的false值。

逻辑运算符为:and, or, not

and的结果为:如果整个表达式的结果是真,那么结果是第二个数,否则是第一个为假数。

or的结果为:如果整个表达式的结果是真,那么结果是第一个为真的数,否则是第二个数。

 > 4 and 5 --> 5
 > nil and 13 --> nil
 > false and 13 --> false
 > 0 or 5 --> 0
 > false or "hi" --> "hi"
 > nil or false --> false

andor都是短路操作符,也就是说第二个操作数只有在必须的时候才会进行判断。

例如在Java中判断数组:

if(arr != null && arr.length > 0)

这样在数组非空的时候才会判断数组长度。

Lua中判空赋值可以这样写:

if not x then x = v end

类似于三元表达式condition : true ? false, 在Lua中可以这样写:

a and b or c

例子:

> 3 and 4 or 5
4
> nil and 4 or 5
5

独立的解释器

独立解释器(也称为lua.c,因为它的源文件或简单的lua是可执行的)是一个小程序,允许直接使用lua。本节介绍它的主要选项。

解释器在加载文件时会忽略以#开头的第一行。在基于POSIX的系统中,这个特性允许我们像使用脚本一样使用Lua,例如:

#!/usr/local/bin/lua

然后我们可以直接调用脚本,而不需要显式的调用Lua解释器。

lua的用法为:

lua [options] [script[args]]

-e选项允许我们在调用命令时直接输入语句执行:

% lua -e "print(math.sin(12))" --> -0.53657291800043

-l选项加载一个library。

-i选项在执行完其他语句后加入交互模式。

在运行它的参数之前,解释器查找一个名为LUA_INIT_5_3的环境变量,如果没有这样的变量,则查找LUA_INIT。如果其中一个变量的内容是@filename,那么解释器将运行给定的文件。如果定义了LUA_INIT_5_3(或LUA_INIT),但是它没有以@开始,那么解释器就假定它包含Lua代码并运行它。在配置独立解释器时,LUA_INIT为我们提供了强大的功能,因为我们在配置中拥有Lua的全部功能。我们可以预加载包、更改路径、定义自己的函数、重命名或删除函数,等等。

script可以让我们获得整条命令调用。例如:

% lua -e "sin=math.sin" script a b

解释器收集参数如下:

 arg[-3] = "lua"
 arg[-2] = "-e"
 arg[-1] = "sin=math.sin"
 arg[0] = "script"
 arg[1] = "a"
 arg[2] = "b"

Number类型

在5.2版之前,Lua使用双精度浮点格式表示所有数字。从version 5.3开始,Lua对数字使用两种替代表示:64位整数(称为简单整数)和双精度浮点数(称为简单浮点数)。(注意,在本书中,术语float并不意味着单一精度。)对于受限平台,我们可以将Lua 5.3编译为Small Lua,它使用32位整数和单精度浮点数。

如果用函数type(),整数和浮点数都表示为number

但是如果用函数math.type(),我们就可以找到它们之间的区别。

> type(3)
number
> type(3.0)
number
> math.type(3)
integer
> math.type(3.0)
float

同样的我们可以使用科学计数法:

> 0.4e-3
0.0004

和其他编程语言不同的是,Lua还支持浮点十六进制常数。 它可以有分数部分和二元指数,以p为前缀。

 > 0x0.2 --> 0.125
 > 0x1p-1 --> 0.5
 > 0xa.bp2 --> 42.75

Lua可以使用string.format方法和%a选项:

 > string.format("%a", 419) --> 0x1.a3p+8, 即0x1A3
 > string.format("%a", 0.1) --> 0x1.999999999999ap-4

虽然不利于我们阅读,但是速率上会更快。

算法操作符

在做加减乘除操作时,会根据操作数的类型做相应的转换,,即如果两个操作数都是整型,那么结果是整型,否则是浮点数。需要注意的是在做除法的时候,即使可以整除,结果也会是浮点类型:

> 4 / 2
2.0

为了得到一个整数,Lua5.3提供了一个新的操作符//,代表整除(floor division向下整除):

> 4//2
2
> 5//2
2

求余符号定义为:

a % b == a - ((a // b) * b)

对于整型操作数而言,和其他语言表现一致。但是对于浮点数则有些意想不到的用处:

 > x = math.pi
 > x - x%0.01 --> 3.14
 > x - x%0.001 --> 3.141

同样的,Lua中也有平方操作符,即:^

> 2 ^ 5
32.0
> 4 ^ 0.5
2.0

关系操作符

Lua提供以下操作符:

< > <= >= == ~=

其中~=是不等号。

数学方法库

Lua为一个标准的数学库提供了一系列的数学函数,包括三角函数((sin, cos, tan, asin等等),对数,舍入功能,最大和最小,一个函数生成伪随机数(random),加上常数π和huge(最大可表示的数量,在大多数平台上通常为inf)。

所有三角函数都以弧度表示。 我们可以使用deg和rad函数来转换度和弧度。

随机数产生器

math.random生成伪随机数。我们可以用三种方式来调用它。

  • 当我们在没有参数的情况下调用它时,它会返回一个在区间[0,1]内均匀分布的伪随机实数。
  • 当我们只使用一个参数(整数n)调用它时,它将返回一个区间[1,n]内的伪随机整数。例如,我们可以用call random(6)模拟掷骰子的结果。
  • 最后,我们可以使用两个整型参数l和u来调用random,从而得到一个区间[l,u]内的伪随机整数。

我们可以用randomseed函数设置伪随机生成器的种子;它的唯一数值参数是seed。当程序启动时,系统用固定种子1初始化生成器。如果没有另一个种子,程序的每次运行都会生成相同的伪随机数序列。对于调试来说,这是一个很好的特性;但在游戏中,我们会不断地看到同样的场景。解决此问题的一个常用技巧是使用当前时间作为种子,并调用math.randomseed(os.time())

取整函数

  • floor:向负无穷取整
  • ceil:向正无穷取整
  • modf:向零取整
> math.floor(3.3)
3
> math.floor(-3.3)
-4
> math.ceil(3.3)
4
> math.ceil(-3.3)
-3
> math.modf(3.3)
3	0.3
> math.modf(-3.3)
-3	-0.3
>

表现限制

大多数编程语言都是用固定位数表示数字的。因此,这些表示在范围和精度上都有限制。

标准Lua使用64位整数… 具有64位的整数可以表示高达2^63-1的值,大约10^19. (Small Lua使用32位整数,可以计算大约200亿。) math库定义了具有整数最大值(math.maxinteger)和最小值(math.mininteger)值的常量。

Lua是环绕整数:

> math.maxinteger == math.mininteger - 1
true
> math.maxinteger - 1 == math.mininteger - 2
true

改为加浮点数时,情况就有些不同:

> math.maxinteger
9223372036854775807
> math.maxinteger + 1
-9223372036854775808
> math.mininteger
-9223372036854775808
> math.maxinteger + 1.0
9.2233720368548e+18
> math.maxinteger + 2.0
9.2233720368548e+18
> math.maxinteger + 1.0 == math.maxinteger + 2.0
true

转换

整型-》浮点数:

> 3 + 0.0
3.0

浮点数-》整型:

> math.tointeger(-258.0)
-258

优先级

从高到低:

 ^
 unary operators (- # ~ not)
 * / // %
 + -
 .. (concatentation)
 << >> (bitwise shifts)
 & (bitwise AND)
 ~ (bitwise exclusive OR)
 | (bitwise OR)
 < > <= >= ~= ==
 and
 or

所有的二元运算符都是左结合的,除了指数运算和连接运算,它们是右结合的。

String类型

Lua中String同样是不可变的。

Lua中String是自动内存管理的,和其他Lua对象一样(tables, functions等)。

获取字符串的长度使用#:

> a = "hello world"
> #a
11

字符串的连接使用的是..:

> a = "hello"
> a
hello
> a .. " world"
hello world
> a
hello

字面量

使用单引号或者双引号都可以:

a = "a line"
b = 'another line'

转义字符和其他语言基本一致。

长字符串

可以使用[[...]]来包含长的字符串,其中的内容不会进行转义:

 page = [[
 
 
   An HTML Page
 
 
   Lua
 
 
 ]]
 
 write(page)

有时我们会遇到多个需要]]的语句,例如a = b[c[i]],或者有时在注释语句时使用]]。为了避免冲突,我们可以在两个括号之间添加任意个=。例如:[=[...]=],

[==[....]==]Lua会自动匹配相等个数的等号。

约束

Lua提供在数字和字符串之间的自动转换。

许多人认为,这些自动约束在Lua的设计中不是一个好主意。 一般来说,最好不要指望他们。 它们在一些地方很方便,但同时也增加了复杂性。

作为这种“二等地位”的反映,Lua5.3没有实现约束和整数的完全集成,而是倾向于更简单、更快的实现。 算术运算的规则 结果是,只有当两个操作数都是整数时,结果才是整数;字符串不是整数,因此任何带有字符串的算术运算都被处理为浮点运算:

> "3" + 4
7.0

tonumber提供了便捷的字符串转数字功能:

 > tonumber(" -3 ") --> -3
 > tonumber(" 10e4 ") --> 100000.0
 > tonumber("10e") --> nil (not a valid number)
 > tonumber("0x1.3p-4") --> 0.07421875

tonumber同时可以提供进制转换功能:

 > tonumber("100101", 2) --> 37
 > tonumber("fff", 16) --> 4095
 > tonumber("-ZZ", 36) --> -1295
 > tonumber("987", 8) --> nil

tostring方法将数字转为字符串。

与算术运算符不同,顺序运算符从不强迫它们的参数。 记住,“0”与“0”不同。 此外,2<15显然是对的,但“2”<“15”是假的(按字母顺序排列)。 为了避免不一致的结果,Lua会提示一个错误,当我们混合字符串和数字的顺序比较,如2<“15”。

String库

string库提供一些便捷的方法:

 > string.rep("abc", 3) --> abcabcabc
 > string.reverse("A Long Line!") --> !eniL gnoL A
 > string.lower("A Long Line!") --> a long line!
 > string.upper("A Long Line!") --> A LONG LINE!

sub(s, i, j)代表获取s从i到j的子字符串。需要注意的是,第一个字符下标为1。最后一个字符为-1。

 > s = "[in brackets]"
 > string.sub(s, 2, -2) --> in brackets
 > string.sub(s, 1, 1) --> [
 > string.sub(s, -1, -1) --> ]

函数string.charstring.byte在字符及其内部数值表示之间进行转换。函数string.char得到零个或多个整数,将每个整数转换为一个字符,并返回连接所有这些字符的字符串。 调用string.byte(s,i)返回strings第i个字符的内部数值表示;第二个参数是可选的; 调用string.byte返回s的第一个(或单个)字符的内部数字表示。调用string.byte(s,i,j)返回i到j字符的ASCII码。

> string.char(97)
a
> string.byte("abc")
97
> string.byte("abc", 2)
98
> string.byte("abc", 1, -1)
97	98	99

string.format是格式化字符和转换数字为字符串的函数。输出和C的printf类似。

d代表十进制整型,x代表十六进制,f代表浮点数,s代表字符串。

 > string.format("x = %d y = %d", 10, 20) --> x = 10 y = 20
 > string.format("x = %x", 200) --> x = c8
 > string.format("x = 0x%X", 200) --> x = 0xC8
 > string.format("x = %f", 200) --> x = 200.000000
 > tag, title = "h1", "a title"
 > string.format("<%s>%s", tag, title, tag)
 --> 

a title

> string.format("pi = %.4f", math.pi) ---> pi = 3.1416

我们可以使用冒号操作符将字符串库中的所有函数作为字符串上的方法调用。例如,我们可以重写string.sub(s, i, j)s:sub(i, j); string.upper(s)变为s:upper()

使用string.find来查找子串:

> string.find("hello world", "wor")
7	9
> string.find("hello world", "wir")
nil

使用string.gsub来替换子串:

 > string.gsub("hello world", "l", ".") --> he..o wor.d 3
 > string.gsub("hello world", "ll", "..") --> he..o world 1
 > string.gsub("hello world", "a", ".") --> hello world 0

Unicode

函数reverse、upper、lower、bytechar不适用于UTF-8字符串,因为它们都假设一个字符等于一个字节。函数string.formatstring.rep除了格式选项“%c”(假定一个字符是一个字节)外,使用UTF-8字符串时不会出现问题。 string.lenstring.sub使用也不会有问题,因为它们是索引引用字节计数(不是字符计数)。

 > utf8.len("résumé") --> 6
 > utf8.len("ação") --> 4
 > utf8.len("Månen") --> 5
 > utf8.char(114, 233, 115, 117, 109, 233) --> résumé
 > utf8.codepoint("résumé", 6, 7) --> 109 233

Table

Lua中的table代表array,set,record和一些其他数据结构。Lua使用table也代表包和对象。

table是一种数组不仅仅只接受数字作为下标,也可以接受string或者其他类型的值(除了nil)。

事实上,table在Lua中不是值或者变量,而是对象。可以将table看作是动态分配的对象(Java中的ArrayList);程序只操作对它们的引用(或指针)。Lua从不在幕后隐藏副本或创建新表。

我们创建table代表着一种构造表达式,简单的写为{}:

 > a = {} -- 创建表格并分配内存
 > k = "x"
 > a[k] = 10 -- new entry, with key="x" and value=10
 > a[20] = "great" -- new entry, with key=20 and value="great"
 > a["x"] --> 10
 > k = 20
 > a[k] --> "great"
 > a["x"] = a["x"] + 1 -- increments entry "x"
 > a["x"] --> 11

table下标

表格的下标可以同时是不同类型的值,同时会自动增长和分配内存。

> a = {}
> a[0] = 10
> a["x"] = "xxx"
> a

Lua提供一种语法糖。即使用.name来替代["name"]

> a = {}
> a[0] = 10
> a["x"] = "xxx"
> a[5]
nil
> a.0
stdin:1: syntax error near '.0'
> a.x
xxx

点表示法清楚地表明,我们使用表作为结构,其中有一组固定的预定义键。字符串表示法给出的概念是,table可以有任何字符串作为键,出于某种原因,我们正在操作那个特定的键。

table的构造方法

构造方法是创建和初始化table的表达式。

最简单的方式是使用{}

创建一个list的方式如下:

 days = {"Sunday", "Monday", "Tuesday", "Wednesday",
 "Thursday", "Friday", "Saturday"}
 
 print(days[4]) --> Wednesday

需要注意的是下标是从1开始的。

Lua提供特定的格式创建一个record-like的table:

a = {x = 10, y = 20}
-- 等于以下创建方式
a = {}; a.x = 10; a.y = 20;

然而,原始表达式更快,因为Lua已经创建了具有正确大小的表。

移除一个字段只需要将其变为nil

创建一个具有适当构造函数的表是更有效的,而且更清晰。 我们可以在同一个构造函数中混合记录样式和列表样式初始化:

polyline = {color="blue",
 thickness=2,
 npoints=4,
 {x=0, y=0}, -- polyline[1]
 {x=-10, y=0}, -- polyline[2]
 {x=-10, y=1}, -- polyline[3]
 {x=0, y=1} -- polyline[4]
 }

这些构造函数形式有其局限性。 例如,我们不能用负索引初始化字段,也不能用不是正确标识符的字符串索引初始化字段。 为了满足这些需要, 另一种,更一般的格式。 在这种格式中,我们显式地将每个索引作为表达式写入方括号中:

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

这种语法更麻烦,但也更灵活:列表样式和记录样式的形式都是这种更一般语法的特殊情况,如我们在下面的等价表示:

 {x = 0, y = 0} <--> {["x"] = 0, ["y"] = 0}
 {"r", "g", "b"} <--> {[1] = "r", [2] = "g", [3] = "b"}

我们总是可以在最后一个条目后面放一个逗号。 这些尾随逗号是可选的,但始终有效:

a = {[1] = "red", [2] = "green", [3] = "blue",}

Arrays, lists, sequences

在table中使用整型作为下标便可以实现array,list,sequence的效果。

需要注意的是,任何未初始化的键其值皆为nil,而不是像Java那样为0。

Lua中1是开始的下标。

序列是table的键从1开始,不中断的一串数字,类似于:{1, 2, 3...n-1, n},并且每个键对应的值不为nil。对于sequence(序列),Lua提供长度操作符:#

table截断

使用pairs可以将表转换成键值对:

t = {10, print, x = 12, k = "hi"}
 for k, v in pairs(t) do
 print(k, v)
 end
 --> 1 10
 --> k hi
 --> 2 function: 0x420610
 --> x 12

由于Lua实现表的方式,元素在遍历中出现的顺序是未定义的。 同一程序每次运行都能产生不同的顺序。唯一确定的就是 元素将在遍历过程中出现一次。

对于列表,我们可以使用ipair迭代器,这保证了每次遍历的顺序结果一致:

t = {10, print, 12, "hi"}
 for k, v in ipairs(t) do
 print(k, v)
 end
 --> 1 10
 --> 2 function: 0x420610
 --> 3 12
 --> 4 hi

另外一种遍历序列的方式如下:

 t = {10, print, 12, "hi"}
 for k = 1, #t do
 print(k, t[k])
 end
 --> 1 10
 --> 2 function: 0x420610
 --> 3 12
 --> 4 hi

安全的定位

考虑以下情形,我们拥有一个库lib然后我们才能调用lib.foo方法。如果我们不确信lib库是否存在。我们就要写成if lib and lib.foo then,先判断库是否存在再判断方法是否存在。如果有很多级的看起来就会很长很复杂,C#提供?.表达式作为安全定位的使用,即原本的判断可以写成:if lib ?. fooLua可以这样实现安全的定位:

(lib or {}).foo

table库

table库提供了一些有用的功能操作list和sequence。

table.insert类似于Java中的list.add

> a = {10, 20, 40}
> table.insert(a, 50)
> for k,v in pairs(a) do
>> print(k, v)
>> end
1	10
2	20
3	40
4	50
> table.insert(a, 1, 5)
> for k,v in pairs(a) do
print(k, v)
end
1	5
2	10
3	20
4	40
5	50

同样的还有方法table.remove。通过这两个方法我们可以实现栈,队列,双端队列。

Lua5.3提供table.move(a, f, e, t)代表着在table a中移动f到e下标的元素到t。

> a = {1, 2, 3, 4, 5, 6, 7, 8, 9}
> table.move(a, 1, 4, 9)
table: 0xefa0b0
> for k, v in pairs(a) do
>> print(k, v)
>> end
1	1
2	2
3	3
4	4
5	5
6	6
7	7
8	8
9	1
10	2
11	3
12	4

可以看到实际上move的行为就是copy值过去。例如:

table.move(a, 1, #a, 1, {}) -- 复制a
table.move(a, 1, #a, #b + 1, b) -- 复制a到b中

function类型

一般情况下函数都要有(),只有在函数只有一个参数的时候可以选择省略括号:

print "hello"  -- 等价于 print("hello")

Lua处理参数通过下面这个例子可见一斑:

 function f (a, b) print(a, b) end

 f() --> nil nil
 f(3) --> 3 nil
 f(3, 4) --> 3 4
 f(3, 4, 5) --> 3 4 (5 is discarded)

多个结果

Lua的一个非常规但相当方便的特点是函数可以返回多个结果。

s, e = string.find("hello Lua users", "Lua")
print(s, e) --> 7 9

在例如,我们写一个程序返回数组中的最大值和最大值的坐标:

function max(t)
  local maxIndex = 1
  local max = t[maxIndex]
  for i = 1, #t do
    if t[i] > max then 
      max = t[i]; maxIndex = i;
    end
  end
  return max, maxIndex
end

lua总是根据调用的情况调整函数的结果数量。 当我们将函数称为语句时,Lua丢弃了函数的所有结果。 当我们把调用当作 表达式(例如,加法的操作数),Lua只保留第一个结果。 只有当调用是表达式列表中的最后一个(或唯一)表达式时,我们才能得到所有结果。 看下面这个例子:

 function foo0 () end -- returns no results
 function foo1 () return "a" end -- returns 1 result
 function foo2 () return "a", "b" end -- returns 2 results

在多个赋值中,函数调用作为最后(或唯一)表达式产生尽可能多的结果来匹配变量:

 x, y = foo2() -- x="a", y="b"
 x = foo2() -- x="a", "b" is discarded
 x, y, z = 10, foo2() -- x=10, y="a", z="b"

在多个赋值中,如果一个函数的结果比我们需要的少,Lua将为缺失的值生成nils:

x,y = foo0() -- x=nil, y=nil
x,y = foo1() -- x="a", y=nil
x,y,z = foo2() -- x="a", y="b", z=nil

请记住,只有当调用是列表中的最后一个(或唯一)表达式时,才会发生多个结果。 不是列表中最后一个元素的函数调用总是产生一个结果:

 x,y = foo2(), 20 -- x="a", y=20 ('b' discarded)
 x,y = foo0(), 20, 30 -- x=nil, y=20 (30 is discarded)

当函数调用是另一个调用的最后(或唯一)参数时,第一个调用的所有结果都作为参数。

 print(foo0()) --> (no results)
 print(foo1()) --> a
 print(foo2()) --> a b
 print(foo2(), 1) --> a 1
 print(foo2() .. "x") --> ax (see next) 

构造器收集所有的结果, 与往常一样,只有当调用是列表中的最后一个表达式时,这种行为才会发生;任何其他位置的调用都会产生一个精确的结果::

 t = {foo0()} -- t = {} (an empty table)
 t = {foo1()} -- t = {"a"}
 t = {foo2()} -- t = {"a", "b"}
 t = {foo0(), foo2(), 4} -- t[1] = nil, t[2] = "a", t[3] = 4

可变参数函数

例如一个求和函数:

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

print(add(2, 3, 4, 5)) -- 14

使用{...}收集可变参数为list。在罕见的情况下,额外的语句可以是有效的nils,用{...}创建的表可能不是一个适当的序列。例如,无法在这样的表中检测原始参数中是否存在尾随nils。在这些场合,Lua提供table.pack功能。此功能接收 任何数量的参数,并返回一个新表及其所有参数,但此表还有一个额外的字段n,代表参数总个数。

 function nonils (...)
   local arg = table.pack(...)
   for i = 1, arg.n do
     if arg[i] == nil then return false end
   end
   return true
 end
 
 print(nonils(2,3,nil)) --> false
 print(nonils(2,3)) --> true
 print(nonils()) --> true
 print(nonils(nil)) --> false

另一个遍历可变参数的方法是selectselect(n, ...)代表查看第n个元素及以后的参数,其中select(“#”, ...)代表获取所有元素的个数。

 print(select(1, "a", "b", "c")) --> a b c
 print(select(2, "a", "b", "c")) --> b c
 print(select(3, "a", "b", "c")) --> c
 print(select("#", "a", "b", "c")) --> 3

所以我们的求和方法也可以写成:

function add(...)
  local sum = 0
  for i = 1, select("#", ...) do
    sum = sum + select(i, ...)
  end
  return sum
end

print(add(2, 3, 4, 5))

对于较少的参数,第二个版本的add更快,因为它避免了在每次调用时创建一个新表。 但是,对于更多的参数,使用多个参数进行选择的多次调用的成本要高于创建表的成本,因此第一个版本是更好的选择。(特别是,第二个版本的成本是二次的,因为迭代的次数和每次迭代中传递的参数的数量都随着参数的数量增加而增加。)

table.unpack方法

table.pack相反,unpack接受list返回一组元素。

print(table.unpack{10,20,30}) --> 10 20 30

unpack的一个重要用途是在泛型调用机制中。泛型调用机制允许我们动态地调用任何具有任何参数的函数。 例如,在ISO C中,没有办法使用泛型调用,但是我们可以声明一个函数,它接受一个可变数量参数(用stdarg.h),我们可以使用指向函数的指针调用一个变量函数。 但是,我们不能用可变参数调用函数:在C中写入的每个调用都有固定的参数,每个参数都有固定的类型。 在Lua,如果我们想调用带有可变数量参数的函数,我们只需编写如下:

f(table.unpack(a))

unpack还可以带参数限制个数:

print(table.unpack({"Sun", "Mon", "Tue", "Wed"}, 2, 3))
 --> Mon Tue

尽管这个函数预定义在C中,我们也可以尝试用Lua实现:

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

适当的尾部调用

Lua是正确的尾递归,尽管这个概念并不直接涉及递归。

尾调用意味着函数最后一步是调用其他函数。例如:

function f(x) x = x + 1; return g(x) end

调用g即尾部调用。这意味着当g函数返回时可以直接返回给f,不需要再使用其他栈空间。

因为尾调用不使用堆栈空间,所以程序可以进行的嵌套尾调用的数量是无限的。举个例子:

function foo(n) 
  if n > 0 then return foo(n - 1) end
end

栈永远不会溢出。

需要注意的是这种尾递归调用的机制在于程序只需要得到g的结果便可以返回。以下几种都不是正确的尾递归:

function f(x) g(x) end   -- 得到g(x)的操作然后丢弃
function f(x) return g(x) + 1 end  -- 得到g(x)的值后加一
function f(x) return x or g(x) end
function f(x) return (g(x)) end

只有这种格式是尾调用return func(args)。这里func和args可能是复杂的表达式,但这不影响它是尾调用。

return x[i].foo(x[j] + a * b, i + j)

外面的世界

由于强调可移植性和可嵌入性,Lua本身在与外部世界通信方面提供的功能并不多。在真正的Lua程序中,大多数I/O要么由主机应用程序完成,要么通过主发行版中不包含的外部库(从图形到数据库和网络访问)完成。纯Lua只提供ISO C标准所提供的功能—即基本的文件操作和一些附加功能。在本章中,我们将看到标准库如何覆盖这些功能。

简单的IO模型

I/O库提供了两种不同的文件操作模型。 该简单模型假设当前输入流和当前输出流,其I/O操作在这些流上操作。将当前输入流初始化为进程的标准输入(stdin),将当前输出流初始化为进程的标准输出(stdout)。 因此,当我们执行类似io.read(),我们从标准输入中读取一行。

我们可以改变这些当前的数据流使用功能io.inputio.output。像io.input(filename)这样的调用以读模式打开给定文件上的流,并将其设置为当前输入流。从现在开始,所有的输入都将来自这个文件,直到再次调用io.input。output对输出做类似的工作。如果出现错误,这两个函数都会引起错误。如果您想直接处理错误,您应该使用完整的I/O模型。

由于写要比读简单,我们先来看看它。函数io.write只是获取任意数量的字符串(或数字)并将它们写入当前输出流。因为我们可以用多个参数调用它,所以我们应该避免像io.write(a..b..c)这样的调用;调用io.write(a, b, c)用更少的资源实现了相同的效果,因为它避免了连接操作。

作为一种规则,您应该只在快速运行或调试程序时使用print;通常情况下应当使用io,当你需要完全控制你的输出时。与print不同,write不向输出添加额外的字符,例如制表符或换行符。此外,io.write允许重定向输出,而print总是使用标准输出。最后,print自动将tostring应用到它的参数上;这对于调试很方便,但是也会隐藏一些细微的错误。

io.write转换数字为字符串按照通常的规则,因此我们想要格式化一下:

 > io.write("sin(3) = ", math.sin(3), "\n")
 --> sin(3) = 0.14112000805987
 > io.write(string.format("sin(3) = %.4f\n", math.sin(3)))
 --> sin(3) = 0.1411

io.read函数从输入流中读字符串,以下参数决定了读取的内容:

  • “a”: 读整个文件
  • “l”:读下一行(删除新行)
  • “L”:读下一行(保留新行)
  • “n":读一个数字
  • num:读取num个字符作为一个字符串

例如,读取一个文件并且替换当中的某些字符:

 t = io.read("a") -- read the whole file
 t = string.gsub(t, "bad", "good") -- do the job
 io.write(t) -- write the file

完整的I/O模型

使用io.open打开一个文件,其中r代表读,w代表写(覆盖写),a代表添加,b代表打开二进制文件。当发生错误时,open返回nil,错误信息和系统依赖的错误总数:

 print(io.open("non-existent-file", "r"))
 --> nil non-existent-file: No such file or directory 2
 
 print(io.open("/etc/passwd", "w"))
 --> nil /etc/passwd: Permission denied 13

检查错误的一个典型成语是使用函数断言:

local f = assert(io.open(filename, mode))

如果打开失败,错误消息作为第二个参数来断言,然后显示消息。

打开文件后,我们可以用read, write方法从结果流中读取或写入。 它们类似于读写函数,但我们将它们称为流ob上的方法,使用冒号运算符。 例如,要打开一个文件并全部读取它,我们可以使用这样的片段:

local f = assert(io.open(filename, mode))
local t = f:read("a")
f:close()

I/O库预定义C流,称为:io.stdin, io.stdout, io.stderr,例如我们可以直接将一个消息送到错误流中:

io.stderr.write(message)

函数io.inputio.output允许我们将完整的模型与简单的模型混合。 我们通过调用io.input()获得当前输入流。 我们调用io.input(handler)方法使用流。 (类似的调用也适用于io.output) 例如,如果我们想暂时改变当前的输入流,我们可以写这样的东西:

local temp = io.input() -- 保存当前流
io.input("newinput") -- 打开新的当前流
io.input():close() -- 关闭当前流
io.input(temp) -- 恢复先前的流

请注意,io.read(args)实际上是io.input():read(args)的简写形式,即在当前输入流上应用的read方法。类似地,io.write(args)io.output():write(args)的简写。

其他关于文件的操作

其他系统调用

os.exit关闭当前程序的执行。可选的第一个参数返回程序的状态,可以是数值(0代表成功),也可以是Boolean值(true代表成功)。可选的第二个参数如果是true,关闭Lua的状态,调用所有的析构器和释放所有表示状态的内存。

os.getenv获取当前环境变量的值。例如:

print(os.getenv("HOME"))  ---> /home/lua

执行系统命令

功能os.execute执行系统命令,等同于C中的system。它接受命令的字符串,并返回有关命令如何终止的信息。第一个结果是一个布尔值(true表示程序退出时没有错误)。第二个结果是一个字符串:如果程序正常终止,则为“exit”(如果程序被某个信号中断,则为“signal”)。第三个结果是返回状态(如果程序正常终止)或终止程序的信号的数量。例如,在POSIX和Windows中,我们都可以使用下面的函数来创建新的目录:

function createDir(dirname)
  os.execute("mkdir " .. dirname)
end

另一个常用的功能是io.popen。它运行流通命令,同时连接命令的输入输出流。

-- 如果是POSIX相同,使用ls替代dir
local f = io.popen("dir /B", "r")
local dir = {}
for entry in f:lines() do
  dir[#dir + 1] = entry
end

填充一些缺漏

本地变量和块

默认情况下lua中的变量都是全局的。本地变量的声明在其块内有效。本地变量用local声明。大致上和其他语言的块一致。在lua中,do-end可以定义一个块,类似于Java中的{}

local x1, x2
do
  local a2 = 2*a
  local d = (b^2 - 4*a*c)^(1/2)
  x1 = (-b + d) / a2
  x2 = (-b - d) / a2
end 
print(x1, x2)

尽可能使用局部变量是一种很好的编程风格。局部变量避免用不必要的名称把全局环境弄得乱七八糟;它们还可以避免程序不同部分之间的名称冲突。此外,对局部变量的访问比全局变量的访问要快。最后,局部变量在其作用域结束后立即消失,允许垃圾收集器释放其值。

考虑到局部变量比全局变量“更好”,一些人认为Lua应该默认使用局部变量。但是,在默认情况下,local有它自己的一组问题(例如,访问非本地变量的问题)。更好的方法是不使用缺省值,即在使用之前应该声明所有变量。

Lua发行版附带一个严格的模块。用于全局变量检查的lua;如果我们试图在一个函数中分配一个不存在的全局变量,或者使用一个不存在的全局变量,就会产生一个错误。在开发Lua代码时使用它是一个好习惯。

一个好的用法类似于:

local foo = foo

这里等式后面的foo是原本定义的全局变量。这代表着拷贝一个foo副本到局部中。

有些人认为在块中间使用声明是一种糟糕的做法。 恰恰相反:只有在需要时才声明变量,我们很少需要在没有初始变量的情况下声明变量 值(因此我们很少忘记初始化它)。此外,我们缩短了变量的范围,增加了可读性。

控制结构

if then elseif else

function compare(x, y)
  if x - y > 0 then 
    return 1
  elseif x - y < 0 then 
    return -1
  else 
    return 0
  end 
end

print(compare(1, 3))
print(compare(1, 0))
print(compare(1, 1))

Lua没有switch。

while do end

local i = 1
while i < 10 do
  print(i)
  i = i + 1
end

repeat until

重复XX直到XX。一定会执行一次。

local i = 1
repeat
  print(i)
  i = i + 1
until i > 10

数字for

for循环包含数字for循环和泛型for循环。数字for循环类似于:

for var = startValue, endValue, step do
  something
end

代表从startValue到endValue,按step间隔执行。

for i = 1, 100, 10 do
  if i % 3 == 0 then 
    print(i)
  end
end
-- 21 51 81

首先,在循环启动之前,对所有三个表达式进行一次评估。 第二,控制变量是for语句自动声明的局部变量,仅在循环内部可见。 一个典型的错误是假设变量在循环结束后仍然存在:

for i = 1, 10 do print(i) end
max = i -- probably wrong!

同样的,break在lua也有效。但是没有continue。

泛型for

泛型for也就是迭代器循环。

break,return, goto

break, return和其他语言一样。

goto语句将程序的执行跳转到相应的标签。 关于goto的争论已经持续了很长时间,有些人甚至在今天争论说他们对编程有害和应该禁止编程语言。 尽管如此,目前的几种语言提供goto,有充分的理由。 它们是一种强大的机制,当小心使用时,只能提高 我们代码的质量。

在Lua中,goto语句的语法非常传统:它是保留字goto,后面是标签名称,可以是任何有效的标识符。 标签的语法是 更复杂:它有两个冒号,后面是标签名称,后面是两个冒号,比如::name::。 这种卷积是有意的,以突出显示程序中的标签。

Lua在goto跳转的位置上有些限制。 首先,标签遵循通常的可见性规则,因此我们不能跳入块(因为块内的标签在块外是不可见的)。 第二,我们不能 跳出一个函数。 (注意,第一条规则已经排除了跳入函数的可能性。) 第三,我们不能跳入局部变量的范围。

一个典型的、行为良好的goto用法是模拟一些你从另一种语言中学到的结构,但在Lua中却没有这样的结构,多层次的中断,多层次的继续 重做、局部错误处理等。 continue语句只是循环块末尾标签的goto;redo语句跳转到块的开头:

while some_condition do
  ::redo::
  if some_other_condition then goto continue
  else if yet_another_condition then goto redo
  end
  some code
  ::continue::
end

Lua规范中的一个有用的细节是,局部变量的作用域以定义变量的块的最后一个非空语句结束;标签被认为是空语句 。 要了解这一细节的有用性,请考虑下一个片段:

while some_condition do
  if some_other_condition then goto continue end
  local var = something
  some code
  ::continue::
end

你可能认为这个goto跳转到了变量var的作用域。但是,continue标签出现在block的最后一个非void语句之后,因此它不在var的作用域内。

goto对于编写状态机也很有用。例如,图8.1“带有goto的状态机示例”显示了一个程序,该程序检查其输入是否有偶数个零。

::s1:: do
  local c = io.read(1)
  if c == '0' then goto s2
  elseif c == nil then print'ok'; return
  else goto s1
  end
end

::s2:: do
  local c = io.read(1)
  if c == '0' then goto s1
  elseif c == nil then print'not ok'; return
  else goto s2
  end
end

goto s1

有更好的方法来编写这个特定的程序,但是如果我们想将有限的自动机自动转换Lua代码(考虑动态代码生成),这种技术是有用的…

迷宫游戏是一个典型的状态机,当前的房间是状态。 我们可以为每个房间使用一个块来实现这个迷宫,使用一个goto从一个房间移动到另一个房间。“迷宫” 游戏“展示了我们如何写一个有四个房间的小迷宫。

goto room1 -- initial room

::room1:: do
  local move = io.read()
  if move == "south" then goto room3
  elseif move == "east" then goto room2
  else
    print("invalid move")
    goto room1 -- stay in the same room
  end
end

::room2:: do
  local move = io.read()
  if move == "south" then goto room4
  elseif move == "west" then goto room1
  else
    print("invalid move")
    goto room2
  end
end

::room3:: do
  local move = io.read()
  if move == "north" then goto room1
  elseif move == "east" then goto room4
  else
    print("invalid move")
    goto room3
  end
end

::room4:: do
  print("Congratulations, you won!")
end

对于这个简单的游戏,你可能会发现一个数据驱动的程序,在那里你用表格描述房间和动作,是一个更好的设计。 然而,如果游戏有几个特殊的情况在每个 房间,那么这种状态机设计是相当合适的。

你可能感兴趣的:(Programming,in,Lua,4th(不完全翻译))