Help:Lua

来自滚动的天空Wiki

Lua是轻量的动态类型语言。MediaWiki的Scribunto扩展允许页面调用Lua代码,其中Lua代码放置在“Module:”命名空间下。MediaWiki使用的Lua版本为5.1,但进行了一些修改,并移植了之后的版本的一些功能。

本帮助页面将简单地介绍Lua的语法、用法及特点等,但不会特别细致,建议有一定编程基础的用户阅读。请注意,本页提及了一些MediaWiki及其扩展提供的库,这些在MediaWiki之外的标准运行环境中是不直接具备的。本页还会提及本wiki中的(包括搬运的)部分模块,不一定适用于其他基于MediaWiki的网站。零基础的用户如需学习Lua语言,可在网上自行搜索入门教程。

变量[编辑源代码]

Lua的变量分为全局变量局部变量。局部变量应使用local关键字进行声明。同其他语言一样,我们使用等号进行赋值。在没有声明局部变量的情况下进行赋值,则默认赋值给全局变量。例如,

a = 1 -- 声明一个全局变量
do -- 一个作用域
    local b = 1 -- 声明一个局部变量
    b = "2" -- 修改一个局部变量
end
print(b) -- 局部变量超出作用域会失效,所以为nil

注意:

  1. 上面这个例子中,只是标准的开发环境,因此使用了全局变量,并使用print函数以将值输出到控制台。MediaWiki不同于标准的开发环境,不应当修改全局变量,也不应该使用print函数。
  2. 在MediaWiki的模块中,不应该创建或修改任何全局变量,而应该定义局部变量,并在模块末尾使用return语句返回你需要返回的值。

局部变量也可以先声明再赋值。没有赋值的变量,以及不存在的变量,其值都为nil,使用这些变量并不会抛出错误,但仍需留意。

变量可以多重赋值,例如a, b = 1, 2是有效的,但不支持a = b = 1这样的语法。等号右边也可以是返回多个值的函数(如unpack或string.find)。如果等号右边的值的个数超过左边,则多余的会被忽略;如果少于左边的,则缺少的会赋值为nil。注意Lua没有元组(tuple)。

变量名称不能以数字开头,不能为保留字,也不可以有中文。

数据类型[编辑源代码]

Lua是动态类型语言,变量没有类型,只有值有。声明局部变量、函数参数等,均无需声明类型。

基本类型介绍[编辑源代码]

在Scribunto扩展中,Lua只有6个基本的类型:

nil[编辑源代码]

空值(nil)表示这个值不存在。尝试访问不存在的变量或表中不存在的字段的值时,会得到nil。函数调用中未提供的参数的值都会被视为nil。很多函数也会在失败时返回nil而不是抛出错误,例如tonumber函数在无法转换成数字时会返回nil。

boolean[编辑源代码]

布尔值(boolean)只有true或false。将其他数据类型转化为布尔值时,只有false和nil视为false,其他的一律视为true。

string[编辑源代码]

字符串(string)为一系列的8比特字节;这取决于应用程序以哪种特定的编码来解析。字符串使用单引号或双引号表示,单引号和双引号无区别,也不会区分“字符”与“字符串”。字符串可以转义。

字符串也可以使用长括号来定义。长括号之间可以夹杂着0个或更多个等号(两边的等号要等量)。例如,"abc"'abc'[[abc]][==[abc]==]都是等价的。注意:由长括号定义的字符串可以直接换行,且不会处理转义序列

Lua标准库中的string库可用于处理字符串,MediaWiki库中的mw.textmw.ustring则提供了更多的处理方法。

number[编辑源代码]

Lua中的数字(number)均为双精度浮点数,-9007199254740992到9007199254740992之间的整数都会准确表达,更大的数和带有小数部分的数将会受到舍入的误差。

数字可以用点(.)来表示小数,例如123456.78。数字也可以用不带空格的科学计数法,例如1.23e-10、123.45e20或者1.23E5。也可以用16进制表示整数,方法就是以0x开头,例如0x3A。

1/0会得到inf,0/0会得到nan。inf和nan都可以正确地储存,但是并无直接的文字表示方法(无穷大可以使用math.huge表示)。使用时,应当留意inf和nan可能带来的潜在错误。nan不等于它自己,也就是说,nan == nan会返回false

Lua标准库中的math库可用于处理数字。

table[编辑源代码]

表(table)是复合数据类型,是Lua中最复杂的一种数据类型,很像JavaScript中的对象(object)或者Python中的字典(dict)。在Lua中,表同时充当了映射、列表、集合的功能。

表由多个(或者零个)字段组成,每个字段都有键(key)值(value),其中键可以是除nil或nan外的任何值。不存在的字段可视为其值为nil。键是唯一的,值不是唯一的。若键为表或函数,则判断其唯一性是根据其是否为同一个对象。

声明一个表的方法是:在一组花括号中,使用“[键] = 值”的方式表示一个字段,多个字段使用逗号或分号隔开。如果键是字符串的字面量,且该字符串符合变量名称要求,则可以省略引号并使用“键 = 值”的方式表示。例如,{a = 1, b = "2"}中,"a""b"都是键,1"2"是对应的值,也可以表示为{["a"] = 1, ["b"] = 2}

声明一个表时,键是可以省略的。例如,{"a", "b", "c", "d", "e"}等价于{[1]="a", [2]="b",[3]="c", [4]="d", [5]="e"},这样的表称为数组(array)序列(sequence),其特点就是键是连续不间断的正整数。空表也可以视为数组

对于表t和数字b,如果满足(t == 0 or t[b] ~= nil) and t[b + 1] == nil,则称b为表t的一个边界(border)。数组只有一个边界。有多个边界的表不是严格的数组,称为稀疏数组(sparse array),对其进行取长运算会返回其中一个边界,存在不确定性。例如,{nil, 2, 3, nil, nil, 6}是个稀疏数组,其边界为0、3、6,因此其长度可能是0、3、6中的某一个值。

表也可以用作集合(set),如{a = true, b = true, c = true},因为键是不能重复的,所以可以通过判断键对应的值是否存在来判断集合是否包含某个元素。

表的字段访问:可以通过“变量名称[键]”或“变量名称.键”(该方法的键必须是可作标识符的字符串)的方式来访问表中的键对应的值,也就是说,找到一个键为指定的值(表、函数等需要是同一对象,字符串、数字等只需要值相等即可)的字段,并返回其值。例如,如果t是一个表,则t[true]表示布尔值键true对应的值,t.abct["abc"]表示字符串键"abc"对应的值。如果这个值不存在,返回nil。

表的字段赋值:可以通过赋值语句修改表中的键对应的值。可以给表添加字段、修改现有字段的值或者清除一个字段。例如,t[true] = "q"表示将键true对应的值修改为字符串"q",t["abc"] = nilt.abc = nil(该方法的键必须是可作标识符的字符串)表示将键"abc"对应的值修改为nil(相当于删除这个字段,如果其原来的值不是nil的话)。注意:键可以是除nil或nan外的任意值,可以是函数、表,甚至是表自身,也就是说t[print] = 1t[t] = t这样的用法都是有效的,而t[0/0] = 1则是无效的(0/0相当于nan,参考#number)。

表不会自动复制。例如如果t是一个表,则赋值语句s = t不会将t的内容复制到新的表s中,而是会让变量s和t的指针都指向同一个内存空间,它们仍是同一个对象,对表s的修改和对t的修改是同步、等价的。

Lua中,判断两个表是否相等的标准是两个表是否是同一个对象,而不是内容是否一致。例如,在上面这一段的例子中,s == t就会返回true,而{} == {}会返回false。一般来说,每对花括号都意味着创建了一个新的对象。

Lua标准库中的table库可用于处理表,其中大部分函数都是用于处理数组(表中的数组部分)的。此外,本wiki也搬运了元模块TableTools,可用于对表进行一些高级的处理。

此外,表的索引、赋值、判断相等等操作都可以受到#元表影响。

function[编辑源代码]

Lua中的函数(function)闭包,这意味着它们维护对它们声明的作用域的引用,并可以访问和操作该作用域中的变量。

定义一个函数的基本语法是:

function 函数名称(参数列表)
    语句……
end

函数名称可以是表的字段,例如t.abc。或者,你也可在function前面加上local关键字,表示该函数是个局部变量。参数是可选的,且视为局部变量。定义函数时,可以使用...表示不定数量的参数(参见select)。

函数内可以使用return语句,表示函数返回一个值,return语句后面应当紧跟代码块的结束。如果没有执行return语句,函数不会返回值,可以认为返回了nil。

匿名函数的表示方法为:

function (参数列表)
    语句……
end

匿名函数与普通的函数无区别,例如,下面两个语句的效果是一样的:

  • local function f(x) return x+1 end
  • local f = function (x) return x+1 end

注意:通过匿名函数赋值的方式定义的函数,如果需要在函数内调用函数自身,应当先声明再赋值。例如:

  • 正确:local function f(x) return x < 5 and f(x + 1) or x end
  • 错误:local f = function (x) return x < 5 and f(x + 1) or x end(这里的f(x+1)中的f视为全局变量)
  • 正确:local f; f = function (x) return x < 5 and f(x + 1) or x end

事实上,在Lua中,local function f(...)是一个语法糖,在字节码中,实际上是先声明局部变量再赋值的。

调用函数的语法为:

函数名称(参数列表)

函数的调用可以单独作为语句,也可以作为表达式。此外,如果调用函数时(无论作为语句还是表达式),只有一个参数,且该参数为字符串或表的字面量,则括号可以省略,例如:

  • print "abc"等价于print("abc")
  • print{1, 2, 3}等价于print({1, 2, 3})

Lua的函数不支持方法重载或多重派发,也不能够直接给参数设置默认值。调用函数时,缺失的参数视为nil[1],多余的参数会被忽略。一种常见的间接设置默认值的方式是,检查某个参数的值是否存在,如果为nil则赋值为默认值,例如:

function f(name)
  name = name or 'default'
  ...
end
方法[编辑源代码]

函数作为表的字段时可以作为方法(method)调用,会自动将表自身作为第一个参数。方法是面向对象的一种体现。

Lua并没有严格意义上的方法,但Lua提供了下面两种语法糖:

  • 声明方法:function table:method(args)等价于function table.method(self, args)
  • 调用方法:table:method("foo")等价于table.method(table, "foo")

此外,字符串也可以面向对象使用(参见#元表)。

类型转换[编辑源代码]

Lua的数据类型可以相互转换。

tonumber函数可以将字符串转化为数字,例如tonumber("123")会返回123。不能转化为数字的,则会转化为nil。对字符串进行算数运算时会尝试自动转换成数字。

tostring函数可以将任意值转化为字符串。表会转化为"table"(受#元表影响的除外),函数会转化为"function"。数字与字符串连接、数字之间连接时会自动转换成字符串。

type函数可以返回值的数据类型,用字符串表示。例如type(1/0)会返回"string"type(true)会返回"boolean"

运算[编辑源代码]

和大多数语言一样,Lua支持算术运算。其中,除以零不会出错,会得到inf或nan。模运算的定义为a % b == a - math.floor( a / b ) * b,例如-4%3会返回2而非-1。幂运算的符号为^。对字符串进行算数运算,会尝试使用tonumber函数转化为数字,如无法转换则会抛出错误。

关系运算包括==~=<><=>=,运算结果一定是布尔值。对于==,不同类型的数据比较为false。注意表或函数的比较会比较指针,当两个值指向同一个表或函数时才会返回true,除非受元表影响。

逻辑运算包括andornot。注意:逻辑运算的结果不一定是布尔值,且进行逻辑运算时,并不会提前把所有值算出来,例如3 or error()的值为3,不会抛出错误。可以使用逻辑运算来组成“三元运算”,例如a and b or c相当于JavaScript、Julia或Java中的a ? b : c或Python中的b if a else c,前提是b不能为false。

对两个字符串进行连接运算的方式是a .. b,而非a + ba * b。可以对数字进行连接,连接时会尝试转化为字符串。注意:Lua的字符串是不可变的(immutable),而且Lua不提供任何类型的“字符串构造器(string builder)”,所以反复进行a = a .. b会多次创建新的字符串,并最终将旧字符串作为垃圾收集。如果许多字符串都需要连接,则应使用string.format,或将所有的字符串添加到一个数组然后最后使用table.concat()连接。

#a可以获取字符串或者数组a的长度。对于字符串,会获取其字节数,类似于string.len,获取字符数可以使用函数mw.ustring.len。对于表,只会获取其数组部分的长度,其他部分的字段一律忽略(对于{1, 2, nil, 4}这样的稀疏数组,其长度是其边界中的某一个,例如可能为2或4[2]),如需获取表中所有字段的个数,应使用一次for循环,或导入模块:TableTools并使用其中的size函数。

对于表,上述所有运算都可以通过#元表控制。

优先级[编辑源代码]

Lua中,各运算的优先级如下:

  1. ^
  2. not # -(负号)
  3. * / %
  4. + -(减号)
  5. ..
  6. < > <= >= ~= ==
  7. and
  8. or

此外,对于同级运算,a/b/c相当于(a/b)/ca^b^c相当于a^(b^c)

流程控制[编辑源代码]

Lua和大多数语言一样,具有条件语句和循环语句,没有switch——case语句。

条件语句[编辑源代码]

条件语句的基本语法是:

if 条件1 then
    语句块1
elseif 条件2 then
    语句块2
else
    语句块3
end

当条件1成立时,执行语句块1,否则当条件2成立时,执行语句块2,否则执行语句块3。elseif和else的结构都是可选的,当然elseif可以有多个。

循环语句[编辑源代码]

while循环的基本语法:

while 条件 do
    语句块
end

repeat循环的基本语法:

repeat
    语句块
until 条件

在repeat循环中,语句块至少执行一次。条件可以使用语句块中的局部变量。

for循环的基本语法:

for 名称 = 表达式1, 表达式2, 表达式3 do
    语句块
end

这种循环会声明一个局部变量,由“表达式1”的值开始,以“表达式3”的值为步长,循环到“表达式2”的值。表达式3可以省略,默认为1,但不能为false或nil。

for语句除了用来循环之外,还可以进行迭代。语法为:

for 变量 in 表达式 do
    语句块
end

变量、表达式可以有多个。通常表达式是返回3个值的一个函数调用。如果迭代器函数可以被写入,那么只取决于传入的参数,这会更加有效。否则,在Lua中编程建议返回闭包比返回表更好,因为静态变量在每次迭代更新其成员。

在上述for、repeat和两种for循环中,均可使用break关键字来立即跳出循环,break后面内容不再执行。Lua没有continue语句[3],但可以调用包含循环代码块的函数,用return来代替。

常用的迭代函数有pairsipairsstring.gmatchmw.text.gsplit等。例如,对于一个普通的表t,我们可以使用这样的方式迭代其所有的键和值:

for k,v in pairs(t) do
    含有k和v的语句块
end

元表[编辑源代码]

每个表都可以设置一个元表(metatable),用来修改特定的行为。setmetatable(t, m)可以用来将表t的元表设为m(m也可以为nil,表示取消元表设置)[4],该函数自身也会返回修改后的表;getmetatable(t)可以用来获取表t的元表。

如果元表具有__index字段,则当通过指定的键访问表的字段的时候,如果该字段不存在,则会使用元表__index字段。如果__index是表,则会访问这个表的字段。如果__index是函数,则会调用这个函数。例如,访问t[k]时,如果表本身的这个字段不存在,则会尝试访问__index[k](如果__index是表)或__index(t, k)(如果__index是函数)。rawget函数可绕过这个元方法。

__newindex字段则用于修改给表的字段赋值的行为。赋值给表的一个不存在的键时,如果__newindex字段的值是表,则会在该表中重复赋值。如果__newindex的值是函数,则会调用这个函数。例如,执行t[k] = v时,表本身不存在这个字段,则会执行__newindex[k] = v(如果__newindex是表)或__newindex(t, k, v)(如果__newindex是函数)。rawset函数可绕过这个元方法。

__call字段可以让表能像函数那样调用,值必须是函数。如果这个元表字段存在,则调用表t(...)时,会像__call(t, ...)这样调用,而不是抛出无法调用表的错误。

__metatable字段用于保护元表,防止其被访问或修改。如果该字段存在,当对表进行getmetatable时,不会返回真正的元表,而是返回该字段的值[5],如果进行setmetatable,则会抛出错误[4]

__mode字段用于使表保持弱引用。在Lua中,变量和表的字段对对象(函数、表等)的引用是强引用,只要某个函数和表依然存在引用,例如存储在某个全局变量或者没有超出作用域的局部变量中,或作为键或者值存储在未被清理掉的表中,那么它就不会被垃圾收集机制清理掉,而是一直存储在内存中。弱引用则表示其引用不会阻止该表中存储的内容被清理。当__mode的值为k表示键保持弱引用,v表示值为弱引用,kv表示键和值均为弱引用。例如:

t = setmetatable({value = {1, 2, 3}}, {__mode = 'v'})
print(t.value)
collectgarbage()
print(t.value)

输出的结果为:

table
nil

元表的一些其他字段用法如下,其值一般都是函数,函数的第一个参数是表自身。例如,如果元表__add字段存在,则对表t计算t + a时,则会调用__add(t, a),而不是抛出无法对表进行算术运算的错误。

字段名称和用法 对应操作
__add(self, v) self + v
__sub(self, v) self - v
__mul(self, v) self * v
__div(self, v) self / v
__mod(self, v) self % v
__pow(self, v) self ^ v
__unm(self) -self
__concat(self, v) self .. v
__len(self) #self
__eq(self, v) self == v
__lt(self, v) self < v
__le(self, v) self <= v
__pairs(self)[6] pairs(self)
__ipairs(self)[6] ipairs(self)
__tostring(self) tostring(self)

所有字符串共用同一个元表,该元表的__index字段等同于string库的一个副本,以使得字符串能够作为对象使用。比如,string.match("label123", "^label%d")等价于("label123"):match("^label%d")。注意这个元表无法通过getmetatable函数获得[7]

注释[编辑源代码]

  1. 如果函数有不定数量的参数,则这些参数的数量是准确的;此外一些Lua内置函数也不会补充参数,例如tostring()会抛出错误,而不是理解为tostring(nil)
  2. 需要留意这种情况,因为会导致一些不确定性。
  3. Lua 5.2以上的版本具有标签功能,可达到类似于continue的效果,但MediaWiki使用的Lua 5.1,是不支持标签的。
  4. 4.0 4.1 原版Lua中,debug.setmetatable函数可以强制设置一个值(不限于表)的元表,且不受__metatable元方法影响。MediaWiki删除了此函数。
  5. 原版Lua的debug.getmetatable函数可以强制获取任意值的元表,不受__metatable元方法影响。MediaWiki删除了此函数。
  6. 6.0 6.1 原版Lua 5.1没有__pairs和__ipairs的功能(是在后面的版本才加入的),不过Scribunto扩展在Lua中移植了该功能,因此你可以在MediaWiki中使用此元表。
  7. 原版Lua可以获取任意值的元表,只要元表存在。