Lua(发音: /ˈluːə/,葡萄牙语“月亮”)是一个简洁、轻量、可扩展的脚本语言。Lua有着相对简单的C语言API而很容易嵌入应用中[3]。很多应用程序使用Lua作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性[4]。
编程范型 | 多范型:脚本,指令式(过程式,基于原型,面向对象),函数式 |
---|---|
设计者 | Roberto Ierusalimschy Waldemar Celes Luiz Henrique de Figueiredo |
发行时间 | 1993年 |
当前版本 |
|
类型系统 | 动态、强类型、鸭子 |
实现语言 | ANSI C |
操作系统 | 跨平台 |
许可证 | MIT许可证 |
文件扩展名 | .lua |
网站 | www |
主要实现产品 | |
Lua, LuaJIT, LuaVela, MoonSharp, Luvit, LuaRT | |
派生副语言 | |
Metalua, Idle, GSL Shell, Luau | |
启发语言 | |
Scheme、SNOBOL、Modula-2、CLU、C++ | |
影响语言 | |
GameMonkey, Io, JavaScript, Julia, MiniD, Red, Ring[2], Ruby, Squirrel, MoonScript, C-- |
历史
Lua是在1993年由罗伯托·耶鲁萨林斯希、Luiz Henrique de Figueiredo和Waldemar Celes创建的,他们当时是巴西的里约热内卢天主教大学的计算机图形技术组(Tecgraf)成员。Lua的先驱是数据描述/配置语言“SOL”(简单对象语言)和“DEL”(数据录入语言)[5]。他们于1992年–1993年在Tecgraf独立开发了需要增加灵活性的两个不同项目(都是用于工程应用的交互式图形程序)。在SOL和DEL中缺乏很多控制流结构,需要向它们增加完全的编程能力。
在《The Evolution of Lua》中,这门语言的作者写道[6]:
在1993年,唯一真正的竞争者是Tcl,它已经明确的设计用于嵌入到应用之中。但是,Tcl有着不熟知的语法,未对数据描述提供良好的支持,并且只在Unix平台上运行。我们不考虑LISP或Scheme,因为它们有着不友好的语法。Python仍处在幼年期。在Tecgraf的自由的自力更生氛围下,我们非常自然的尝试开发自己的脚本语言 ... 由于这门语言的很多潜在用户不是专业编程者,语言应当避免神秘的语法和语义。新语言的实现应当是高度可移植的,因为Tecgraf的客户有着非常多样的各种计算机平台。最后,由于我们预期Tecgraf的其他产品也需要嵌入脚本语言,新语言应当追随SOL的例子并提供为带有C API的库。
Lua主要受到了下列前辈语言的影响:
- Modula-2:从中引入了大部分控制结构语法,
if
、while
、repeat
/until
。 - CLU:多赋值和从函数调用的多个返回值,这是对引用参数或显式指针的更简单的替代。
- C++:“允许局部变量只在需要的地方声明的灵巧想法”[6]。
- SNOBOL和AWK:关联数组。
- LISP和Scheme:在发表于《Dr. Dobb's Journal》的文章中,Lua的创立者还声称,有着单一且无所不在的数据结构机制(列表)的LISP和Scheme,对他们决定将表格开发为Lua的主要数据结构起到了主要影响[7]。Lua的语义久而久之日趋受到Scheme的影响[6],特别是介入了匿名函数和完全的词法作用域。
特性
Lua是一种轻量语言,它的官方版本只包括一个精简的核心和最基本的库。这使得Lua体积小、启动速度快。它用ANSI C语言编写[8],并以源代码形式开放,编译后的完整参考解释器只有大约247kB[8],到5.4.3版本,该体积变成283kB(Linux,amd64),依然非常小巧,可以很方便的嵌入别的程序里。和许多“大而全”的语言不一样,网络通信、图形界面等都没有默认提供。但是Lua可以很容易地被扩展:由宿主语言(通常是C或C++)提供这些功能,Lua可以使用它们,就像是本来就内置的功能一样。事实上,现在已经有很多成熟的扩展模块可供选用。
Lua是一个动态类型语言,支持增量式垃圾收集策略。有内置的,与操作系统无关的协作式多线程支持。Lua原生支持的数据类型很少,只提供了数值(默认是双精度浮点数,可配置)、布尔量、字符串、表格、函数、线程以及用户自定义数据这几种。但是其处理表和字符串的效率非常之高,加上元表的支持,开发者可以高效的模拟出需要的复杂数据类型(比如集合、数组等)。
语法和语义
Lua是一种多重编程范型的程序设计语言:它只提供了很小的一个特性集合来满足不同编程范型的需要,而不是为某种特定的编程范型提供繁杂的特性支持。例如,Lua并不提供继承这个特性,但是你可以用元表格来模拟它。诸如命名空间、类这些概念都没有在语言基本特性中实现,但是我们可以用表格结构(Lua唯一提供的复杂数据结构)轻易模拟。正是提供了这些基本的元特性,我们可以任意的对语言进行自需的改造。
Lua实现了少量的高级特征比如头等函数、垃圾回收、闭包、正当尾调用、类型转换(于运行时间在字符串和数值之间自动转换)、协程(协作多任务)和动态模块装载。
经典的Hello World!程序可以写为如下[9]:
print("Hello World!")
或者如下:
print 'Hello World'
print [[Hello World]]
在Lua中注释可以于双连字符并行至此行的结束,类似于Ada、Eiffel、Haskell、SQL和VHDL。多行字符串和注释用双方括号来装饰。
下例中实现了一个阶乘函数:
function factorial(n)
local x = 1
for i = 2, n do
x = x * i
end
return x
end
Lua有一种类型的条件测试:if then end
,它具有可选的else
和elseif then
执行控制构造。
通用的if then end
语句需要三个关键字:
if condition then
--statement body
end
可以增加else
关键字来控制执行,它随同着在if
条件求值为false
之时执行的语句块:
if condition then
--statement body
else
--statement body
end
还可以使用elseif then
关键字依据多个条件来控制执行:
if condition then
--statement body
elseif condition then
--statement body
else -- optional
--optional default statement body
end
Lua有四种类型的循环:while
循环、repeat
循环(类似于do while
循环)、数值for
循环和通用for
循环。
--condition = true
while condition do
--statements
end
repeat
--statements
until condition
for i = first, last, delta do --delta可以是负数,允许计数增加或减少的循环
--statements
--example: print(i)
end
通用for
循环:
for key, value in pairs(_G) do
print(key, value)
end
将在表格_G
上使用标准迭代器函数pairs
进行迭代,直到它返回nil
。
可以有嵌套的循环,就是在其他循环中的循环。
local grid = {
{ 11, 12, 13 },
{ 21, 22, 23 },
{ 31, 32, 33 }
}
for y, row in ipairs(grid) do
for x, value in ipairs(row) do
print(x, y, grid[y][x])
end
end
Lua将函数处理为头等值,在下例子中用print
函数的表现可以修改来展示:
do
local oldprint = print
-- 存储当前的print函数为oldprint
function print(s)
--[[重新定义print函数。新函数只有一个实际参数。
平常的print函数仍可以通过oldprint使用。]]
oldprint(s == "foo" and "bar" or s)
end
end
任何对print
的进一步调用都要经由新函数,并且由于Lua的词法作用域,这个旧的print
函数将只能被这个新的修改了的print
访问到。
Lua还支持闭包,展示如下:
function addto(x)
-- 返回一个把实际参数加到x上
return function(y)
--[=[ 在我们引用变量x的时候,它在当前作用域的外部,
它的生命期会比这个匿名函数短,Lua建立一个闭包。]=]
return x + y
end
end
fourplus = addto(4)
print(fourplus(3)) -- 打印7
--这也可以通过如下方式调用这个函数来完成:
print(addto(4)(3))
--[[这是因为这直接用视件参数3调用了从addto(4)返回的函数。
如果经常这么调用的话会减少数据消耗并提升性能。]]
每次调用的时候为变量x
建立新的闭包,所以返回的每个新匿名函数都访问它自己的x
参数。闭包由Lua的垃圾收集器来管理,如同任何其他对象一样。
function create_a_counter()
local count = 0
return function()
count = count + 1
return count
end
end
create_a_counter()
会返回一个匿名函数,这个匿名函数会把count
加1
后再回传。在匿名函数中的变量count
的值会一直被保存在匿名函数中。因此调用create_a_counter()
时产生一个记数器函数,每次调用记数器函数,都会得到一个比上次大1
的值。
Lua是一种动态类型语言,因此语言中没有类型的定义,不需要声明变量类型,每个变量自己保存了类型。
有8种基本类型:nil、布尔值(boolean)、数字体(number)、字符串型(string)、用户自定义类型(userdata)、函数(function)、线程(thread)和表(table)。
print(type(nil)) -- 输出 nil
print(type(99.7+12*9)) -- 输出 number
print(type(true)) -- 输出 boolean
print(type("Hello Wikipedia")) -- 输出 string
print(type(print)) -- 输出 function
print(type({1, 2, test = "test"})) -- 输出 table
表格(table)是Lua中最重要的数据结构(并且是设计中唯一内置的复合数据类型),并且是所有用户创建类型的基础。它们是增加了自动数值键和特殊语法的关联数组。
表格是键和数据的有序对的搜集,其中的数据用键来引用;换句话说,它是散列异构关联数组。
表格使用{}
构造器语法来创建。
a_table = {} -- 建立一个新的空表格
表格总是用引用来传递的(参见传共享调用)。
键(索引)可以是除了nil
和NaN的任何值,包括函数。
a_table = {x = 10} -- 建立一个新表格,具有映射"x"到数10的一个表项。
print(a_table["x"]) -- 打印于这个字符串关联的值,这里是10。
b_table = a_table
b_table["x"] = 20 -- 在表格中的值变更为20。
print(b_table["x"]) -- 打印20。
print(a_table["x"]) -- 还是打印20,因为a_table和b_table都引用相同的表格。
通过使用字符串作为键,表格经常用作结构(或记录)。由于这种用法太常见,Lua为访问这种字段提供了特殊语法[10]。
point = { x = 10, y = 20 } -- 建立一个新表格
print(point["x"]) -- 打印10
print(point.x) -- 同上一行完全相同含义。易读的点只是语法糖。
通过使用表格来存储有关函数,它可以充当命名空间。
Point = {}
Point.new = function(x, y)
return {x = x, y = y} -- return {["x"] = x, ["y"] = y}
end
Point.set_x = function(point, x)
point.x = x -- point["x"] = x;
end
表格被自动的赋予了数值键,使得它们可以被用作数组数据类型。第一个自动索引是1
而非0
,因为很多其他编程语言都这样(尽管显式的索引0
是允许的)。
数值键1
不同于字符串键"1"
。
array = { "a", "b", "c", "d" } -- 索引被自动赋予。
print(array[2]) -- 打印"b"。在Lua中自动索引开始于1。
print(#array) -- 打印4。#是表格和字符串的长度算符。
array[0] = "z" -- 0是合法索引。
print(#array) -- 仍打印4,因为Lua数组是基于1的。
表格的长度t
被定义为任何整数索引n
,使得t[n]
不是nil
而t[n+1]
是nil
;而且如果t[1]
是nil
, n
可以是0
。对于一个正规表格,具有非nil
值从1
到给定n
,它的长度就精确的是n
,它的最后的值的索引。如果这个数组有“洞”(就是说在其他非nil
值之间的nil
值),则#t
可以是直接前导于nil
值的任何一个索引(就是说可以把任何这种nil
值当作数组的结束[11])。
ExampleTable =
{
{1, 2, 3, 4},
{5, 6, 7, 8}
}
print(ExampleTable[1][3]) -- 打印"3"
print(ExampleTable[2][4]) -- 打印"8"
表格可以是对象的数组。
function Point(x, y) -- "Point"对象构造器
return { x = x, y = y } -- 建立并返回新对象(表格)
end
array = { Point(10, 20), Point(30, 40), Point(50, 60) } -- 建立point的数组
-- array = { { x = 10, y = 20 }, { x = 30, y = 40 }, { x = 50, y = 60 } };
print(array[2].y) -- 打印40
使用散列映射来模拟数组通常比使用真正数组要慢;但Lua表格为用作数组而做了优化来避免这个问题[12]。
可扩展的语义是Lua的关键特征,而“元表格”概念允许以强力方式来定制Lua的表格。下列例子展示了一个“无限”表格。对于任何n
,fibs[n]
会给出第n
个斐波那契数,使用了动态规划和记忆化。
fibs = { 1, 1 } -- 给fibs[1]和fibs[2]初始值。
setmetatable(fibs, {
__index = function(values, n) --[[__index是Lua预定义的函数,
如果"n"不存在则调用它。]]
values[n] = values[n - 1] + values[n - 2] -- 计算并记忆化fibs[n]。
return values[n]
end
})
面向对象编程
尽管Lua没有内置的类的概念,可以用过两个语言特征实现面向对象编程:头等函数和表格。通过放置函数和有关数据入表格,形成一个对象。继承(单继承和多重继承二者)可以通过使用元表格机制来实现,告诉这个对象在哪些父对象中查找不存在的方法。
对于这些技术不采用“类”的概念,而是采用原型,类似于Self或JavaScript。建立新对象要么通过工厂方法(从头构造一个新对象),要么通过复制现存的对象。
建立一个基本的向量对象:
local Vector = {}
Vector.__index = Vector
function Vector:new(x, y, z) -- 构造器
return setmetatable({x = x, y = y, z = z}, Vector)
end
function Vector:magnitude() -- 另一个方法
-- 使用self引用隐蔽的对象
return math.sqrt(self.x^2 + self.y^2 + self.z^2)
end
local vec = Vector:new(0, 1, 0) -- 建立一个向量
print(vec:magnitude()) -- 调用一个方法 (输出: 1)
print(vec.x) -- 访问一个成员变量 (输出: 0)
Lua为便利对象定向提供了一些语法糖。要声明在原型表格内的成员函数,可以使用function table:func(args)
,它等价于function table.func(self, args)
。调用类方法也使用冒号,object:func(args)
等价于object.func(object, args)
。
下面是使用:
语法糖的对应的类:
local Vector = {}
Vector.__index = Vector
function Vector:new(x, y, z) -- 构造子
-- 因为函数定义使用冒号,
-- 其第一个实际参数是"self"
-- 它引用到"Vector"
return setmetatable({x = x, y = y, z = z}, self)
end
function Vector:magnitude() -- 另一个方法
-- 使用self引用隐含的对象
return math.sqrt(self.x^2 + self.y^2 + self.z^2)
end
local vec = Vector:new(0, 1, 0) -- 建立一个向量
print(vec:magnitude()) -- 调用方法(输出:1)
print(vec.x) -- 访问成员变量(输出:0)
Lua支持使用元表格来使得Lua具有类继承[13]。在这个例子中,我们允许向量在派生的类中将它们的值乘以一个常量:
local Vector = {}
Vector.__index = Vector
function Vector:new(x, y, z) -- 构造子
-- 这里的self引用到我们调用"new"方法的任何类
-- 在派生的类中,self将是派生的类;
-- 在Vector类中,self将是Vector。
return setmetatable({x = x, y = y, z = z}, self)
end
function Vector:magnitude() -- 另一个方法
-- 使用self引用隐含的对象
return math.sqrt(self.x^2 + self.y^2 + self.z^2)
end
-- 类继承的例子
local VectorMult = {}
VectorMult.__index = VectorMult
setmetatable(VectorMult, Vector) -- 使得VectorMult成为Vector的孩子
function VectorMult:multiply(value)
self.x = self.x * value
self.y = self.y * value
self.z = self.z * value
return self
end
local vec = VectorMult:new(0, 1, 0) -- 建立一个向量
print(vec:magnitude()) -- 调用一个方法(输出:1)
print(vec.y) -- 访问一个成员变量(输出:1)
vec:multiply(2) -- 将向量的所有分量乘以2
print(vec.y) -- 再次访问成员变量(输出:2)
Lua还支持多重继承,__index
可以要么是一个函数要么是一个表格[14]。也支持运算符重载,Lua元表格通过设置元素比如__add
、__sub
等来重写表的操作符操作行为[15]。
实现
Lua程序不是从文本式的Lua文件直接解释的,而是编译成字节码,接着把它运行在Lua虚拟机上。编译过程典型的对于用户是不可见并且是在运行时间进行的,但是它可以离线完成用来增加装载性能或通过排除编译器来减少对宿主环境的内存占用。Lua字节码还可以在Lua之内产生和执行,使用来自字符串库的dump
函数和load/loadstring/loadfile
函数。Lua版本5.3.4是用大约24,000行C代码实现的[4][8]。
像大多数CPU,而不像多数虚拟机(它们是基于堆栈的),Lua VM是基于寄存器的,因此更加类似真实的硬件设计。寄存器架构既避免了过多的值复制又减少了每函数的指令的总数。Lua 5的虚拟机是第一个广泛使用的基于寄存器的纯VM[16]。Parrot和Android的Dalvik是另外两个周知的基于寄存器的VM。PCScheme的VM也是基于寄存器的[17]。
下面的例子列出上面定义的阶乘函数的字节码(通过luac
5.1编译器来展示)[18]:
function <factorial.lua:1,7> (9 instructions, 36 bytes at 0x8063c60) 1 param, 6 slots, 0 upvalues, 6 locals, 2 constants, 0 functions 1 [2] LOADK 1 -1 ; 1 2 [3] LOADK 2 -2 ; 2 3 [3] MOVE 3 0 4 [3] LOADK 4 -1 ; 1 5 [3] FORPREP 2 1 ; to 7 6 [4] MUL 1 1 5 7 [3] FORLOOP 2 -2 ; to 6 8 [6] RETURN 1 2 9 [7] RETURN 0 1
C API
Lua意图被嵌入到其他应用之中,为了这个目的而提供了C API。API被分成两部分:Lua核心库和辅助库[19]。Lua API的设计消除了对用C代码的手动引用管理的需要,不同于Python的API。API就像语言本身一样是极简主义的。高级功能通过辅助库来提供,它们很大程度上构成自预处理器宏,用以帮助做复杂的表格操作。
Lua C API是基于堆栈的。Lua提供压入和弹出最简单C数据类型(整数、浮点数等)进入和离开堆栈的函数,还有通过堆栈操作表格的函数。Lua堆栈稍微不同于传统堆栈,例如堆栈可以直接的被索引。负数索引指示从栈顶开始往下的偏移量。例如−1是在顶部的(最新进压入的值),而整数索引指示从底部(最旧的值)往上的偏移量。在C和Lua函数之间集结数据也使用堆栈完成。要调用一个Lua函数,把实际参数压入堆栈,并接着使用lua_call
来调用实际的函数。在写从Lua直接调用的C函数的时候,实际参数读取自堆栈。
下面是从C调用Lua函数的例子:
#include <stdio.h>
#include <lua.h> // Lua主要库 (lua_*)
#include <lauxlib.h> // Lua辅助库 (luaL_*)
int main(void)
{
// 建立一个Lua状态
lua_State *L = luaL_newstate();
// 装载并执行一个字符串
if (luaL_dostring(L, "function foo (x,y) return x+y end")) {
lua_close(L);
return -1;
}
// 压入全局"foo"(上面定义的函数)的值
// 到堆栈,跟随着整数5和3
lua_getglobal(L, "foo");
lua_pushinteger(L, 5);
lua_pushinteger(L, 3);
lua_call(L, 2, 1); // 调用有二个实际参数和一个返回值的函数
printf("Result: %d\n", lua_tointeger(L, -1)); // 打印在栈顶的项目的整数值
lua_pop(L, 1); // 返回堆栈至最初状态
lua_close(L); // 关闭Lua状态
return 0;
}
如下这样运行这个例子:
$ cc -o example example.c -llua $ ./example Result: 8
C API还提供一些特殊表格,位于各种Lua堆栈中的“伪索引”之上。Lua 5.2之前[20],在LUA_GLOBALSINDEX
之上是全局表格;_G
来自Lua内部,它是主命名空间。还有一个注册表位于LUA_REGISTRYINDEX
,在这里C程序可以存储Lua值用于以后检索。
还可以使用Lua API写扩展模块。扩展模块是通过向Lua脚本提供本地设施,用来扩展解释器的功能的共享对象。从Lua方面看来,这种模块出现为一个命名空间表格,它持有自己的函数和变量。Lua脚本可以使用require
装载扩展模块[19],就像用Lua自身写的模块一样。一组仍在增长中的叫做“rock”的模块可以通过叫做LuaRocks的软件包管理器获取到[21],类似于CPAN、RubyGems和Python eggs。对大多数流行的编程语言包括其他脚本语言,都存在预先写好的Lua绑定[22]。对于C++,有许多基于模板的方式和一些自动绑定生成器。
应用
在视频游戏开发中,Lua被程序员广泛的用作脚本语言,主要由于它的可感知到的易于嵌入、快速执行,和短学习曲线[23]。
在2003年,GameDev.net组织的一次投票,说明了Lua是游戏编程的最流行脚本语言[24]。在2012年1月12日,Lua被《游戏开发者》宣布为编程工具范畴的Front Line奖2011年度获奖者[25]。
大量非游戏应用也出于可扩展性而使用Lua,比如TeX排版设置语言实现LuaTeX、键-值数据库Redis、文本编辑器Neovim和web服务器Nginx。
通过Scribunto扩展,Lua可获得为MediaWiki软件中的服务器端脚本语言,Wikipedia和其他wiki都基于了它[26][27]。它的应用包括允许从Wikidata集成数据到文章中[28],和助力于自动生物分类框系统。
Lua可以用于嵌入式硬件,不仅可以嵌入其他编程语言,而且可以嵌入微处理器中,例如NodeMCU开源硬件项目将Lua嵌入到Wi-Fi SoC中[29],另外,游戏平台Roblox所有游戏都是利用Lua进行编程的。另外, 还有中国大陆发起的开源LuatOS项目[30],也是将Lua应用到嵌入式领域,但不仅限于Wifi,它包含2G/4G/NBIOT等通信模块,也包含stm32/w800等嵌入式mcu,同时也支持在windows/linux平台上执行的模拟器。
派生语言
- LuaJIT,Lua 5.1的即时编译器[39][40]。
- Luau,由Roblox公司开发,派生自Lua 5.1,具有渐进类型,补充特征并关注于性能[41]。
- Ravi,启用了JIT的Lua 5.3语言并具有可选的静态类型。JIT由类型信息来指导[42]。
- Shine,既有很多扩展的LuaJIT分叉,包括了模块系统和宏系统[43]。
- Glua,嵌入到游戏盖瑞模块中作为其脚本语言的一个Lua修改版本[44]。
- Teal,用Lua编写的静态类型的Lua方言。
此外,Lua用户社区在参考C语言实现之上提供了一些“能力补丁”[45]。
引用
延伸阅读
外部链接
Wikiwand in your browser!
Seamless Wikipedia browsing. On steroids.
Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.
Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.