Lua 基本概念介绍

2020-04-30 lua language

Lua 历史可以从 1993 年开始,到现在为止,在很多场景都可以看到 Lua 的影子,如游戏开发、Web 应用脚本、安全系统插件等,像常见的 nmap、sysbench 等程序。

这里简单介绍 Lua 的安装使用。

简介

在 CentOS 中,Lua 的安装很简单,可以通过 yum install lua 或者源码安装,源码可以直接从 www.lua.org 下载,源码安装详细内容可以参考 doc/readme.html 文件,在 CentOS 中通过如下命令安装。

$ make linux                           # 根据不同的平台选择目标;注意,会依赖readline-devel包
$ make test                            # 测试编译是否正确,简单输出版本信息
# make linux install                   # 安装

安装之后,主要包括了 lualuac 两个命令,其中前者为解析器,后者为编译器。可以直接运行 Lua 交互界面,也可以通过 lua 命令执行脚本,或者先通过 luac 编译为字节码,然后运行。

$ lua                                  # 1.1 使用交互界面
> print('hello world')

$ lua -e "print('hello world')"        # 2.1 运行单条命令
$ cat foobar.lua                       # 2.2 查看文件,也可以为 #!/usr/bin/env lua
#!/usr/bin/lua
print('hello world')
$ lua foobar.lua                       # 2.2 直接运行,两种方式都可以
$ ./foobar.lua

$ luac -o foobar foobar.lua            # 3.1 先编译称字节码
$ lua foobar                           # 3.2 然后运行

另外,可以将 Lua 嵌入到 C/C++ 等语言中,后面详细介绍。

运行

在上述的简介中已经介绍过了部分执行的方式,可以直接解析执行,也可以 “编译” 之后执行,除此之外还可以通过 -i 参数在进入交互程序前先执行一个程序。

----- 先执行一段程序打印Hello World
$ lua -i foobar.lua
Lua 5.1.4  Copyright (C) 1994-2008 Lua.org, PUC-Rio
Hello World
>

另外,在交互程序中,可以通过 dofile() 加载文件,然后可以调用相应的函数。

----- 先加载,然后调用函数
$ cat lib.lua
function trible(x)
    return 3 * x
end
$ lua
Lua 5.1.4  Copyright (C) 1994-2008 Lua.org, PUC-Rio
> dofile("lib.lua")
> print (trible(2))
6

这两个功能在调试程序时非常有效。

语法简介

在 Lua 中,一个文件甚至在交互模式中的一行,都被称为代码块 (chunk),而一个 chunk 是由一系列的命令或者声明组成,与很多语言不同的是,各个语句之间甚至不需要 ; 分割,例如,下面的语句是相同的。

a = 1
b = a*2

a = 1;
b = a*2;

a = 1; b = a*2;

a = 1  b = a*2           # 仍然等效

注释

在 Lua 中,通过两个减号作为行注释,当然,还有块注释。

-- 作为行注释

--[[
    print("foobar") -- 作为块注释
--]]

--[===[
    print("foobar") -- 同样为块注释,等号数不限,防止foo[bar[idx]]类似的异常
--]===]

---[[
    print("foobar") -- 可以直接多加一个字符取消注释
--]]

最后实际上是行注释。

变量

与大多数语言类似,变量名可以为字符串、下划线、数字 (不能以数字开头);不过,需要注意的是,在 Lua 中,以下划线开头通常有特殊的含义,例如 _VERSION_ 等。

另外,需要注意,如果没有特殊说明,均是全局变量,无论是语句块还是在函数中,如果需要局部变量则需要使用 local 关键字。对于全局变量,在使用之前是不需要初始化的,默认值为 nil

而且,Lua 是动态类型的语言 (Dynamically Typed Language),也就是说定义变量时无需指定类型,但是每个值都包含其自己的类型。

Lua 中总共有 8 种基本的类型 nilbooleannumberstringuserdatafunctionthreadtable

print(type(a))                --> nil
print(type(false))            --> boolean
print(type(10.4))             --> number
print(type("Hello world"))    --> string

print(type(print))            --> function

各种类型的详细信息如下:

  • nil 是一个区别于其它值的值,也就是说用来表明该变量还没有被赋值。
  • Booleans 该类型的值只有两个 falsetrue,其它类型的值作比较时均为 false
  • Number 数字类型只有 double(64bits),即使是整形仍然使用 double 表示。
  • Strings 支持 unicode 的字符串,而且可以通过 [[ Long Strings ]] 表示比较长的字符串。
  • Tables 这是 Lua 中提供高效的表示方法,可表示 arrays、sets 等一系列数据结构。
  • Functions 很简单,就是函数了,因为 Lua 解析器是通过 C 编写的,Lua 也可以调用 C 函数。

数值的定义有多种方法,实际上与其它语言类似,例如 1024 3.0 314.16e-2 0.31416E1 0x56 等等。

运算符

与其它语言基本类似,包括了分类、优先级、结合顺序等等,只有部分略有区别,主要包括如下几类:

  • 算术运算符,+ 加法、- 减法、* 乘法、/ 除法、% 取余、^ 乘幂、- 负号。
  • 关系运算符,== 等于、~= 不等于、> 大于、< 小于、>= 大于等于、<= 小于等于。
  • 逻辑运算符,and 与、or 或、not 非。
  • 其它,.. 拼接两个字符串,# 获取字符串长度。

在 Lua 中通过 ~= 表示不等于,而非常见的 !=

Table

在 Lua 中,所谓 Table 其实就是一个 Key-Value 的数据结构,很像 Python 中的字典,不过其 Key 不仅可以是字符串,也可以是数字甚至是浮点数。

t = {name="Andy", [10]=100, [3.1415]="PI", ['age']=30}
print(t.name, t[10], t[3.1415], t.age, t["age"])

如前所述,Lua 中的变量,如果没有 local 关键字,则都是全局变量,所有的全局变量都保存在 _Gtable 中,可以通过下面的方式来遍历。

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

控制语句

也就是 whileif-elsefor 等控制语句。

----- 使用while循环
count = 1
while count < 6 do
    count = count + 1
end

----- 使用if-else控制语句
if op == "+" then
    r = a + b
elseif op == "-" then
    r = a - b
elseif op == "*" then
    r = a * b
else
    error("invalid operation")
end

----- 使用repeat-until语句
repeat
    line = io.read()
until line ~= ""
print(line)

----- 使用for循环,包括了如下的几种方式
for i = 1, 10, 2 do --打印1到10的奇数
    print(i)
end
days = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
for k, v in pairs(days) do
    print(k, v)
end

除了上述的示例之外,还包括了 breakreturngoto 等语句。

函数

在 Lua 中,函数也可以作为一个变量进行赋值:

v = print
v("Hello World")
v = math.sin
v(1)

----- 下面的两个函数一样
function foo(x) return x^2 end
foo = function(x) return x^2 end

Lua 同样支持递归,最简单的就是斐波那契数列求和。

function fib(n)
  if n < 2 then return 1 end
  return fib(n - 2) + fib(n - 1)
end

Lua 同样也支持闭包,简单来说,当有两个函数嵌套时,被嵌套的函数可以访问上层函数保存的变量,函数是用来处理数据的,而闭包同时保存了数据。

----- 使用简单的计数器
function counter()
    local i = 0
    return function()     -- anonymous function
        i = i + 1
        return i
    end
end
c1 = counter()
print(c1())  --> 1
print(c1())  --> 2

----- 计算N次方
function power(N)
    return function(y) return y^N end
end
power2 = power(2)
power3 = power(3)
print(power2(4))       -- 4的2次方
print(power3(5))       -- 5的3次方

类似于 Python、Go 语言,可以在一条语句上赋多个值,函数也可以返回多个值:

name, age = "foobar", 20, "foobar@test.com"
function getUserInfo()
    return "foobar", 20, "foobar@test.com"
end
name, age, email, website = getUserInfo()

上述的赋值中,最后一个字符串将会丢弃;而函数返回时,其中的 website 为 nil 。

模块

从 5.1 开始引入了标准的模块管理机制,是由变量、函数等组成的表,也就意味着,创建模块实际上就是把常量、函数等放入到表中,并将其返回。

-- File: module.lua
-- 保存模块相关的数据
module = {}
-- 定义一个常量
module.constant = "Hello World"
-- 定义可调用的函数
function module.SayHi()
	io.write("Hello World!")
end
-- 私有函数
local function sayWoo()
	print("Wooooo")
end
-- 需要返回该模块
return module

模块就是一个表,可以像操作表元素一样来使用模块里的常量或函数,通过 require 引用模块,此时会返回该模块的表结构,默认会定义一个同名的全局变量,也可通过 local m = require("module") 进行重命名。

require("module")
print(module.constant)
module.SayHi()

每次执行 require 时,会从 Lua 或者 C 库中加载,搜索路径保存在全局变量 package.path 中,默认在初始化时加载 LUA_PATH 环境变量,如果不存在,则使用编译时确定的默认路径初始化,如下是一个示例。

export LUA_PATH="~/lua/?.lua;;"

如果有多个路径,那么就通过 ; 分割,最后的 ;; 表示在新加路径后面加上原来默认路径。

常用模块

常见的模块,以及相关的示例。

Socket

LuaSocket 是 Lua 的网络模块,提供了对 TCP、UDP、DNS、HTTP 等多种网络协议的访问操作,包括了两部分:A) 使用 C 编写的针对 TCP 和 UDP 传输层的访问;B) 用 Lua 编写,负责应用功能的网络接口处理。

相关的源码可以从 GitHub 上下载,在 CentOS 中可以通过 yum install lua-socket 命令安装,不过需要依赖 EPEL 源。安装完成之后可以通过如下方式进行测试,直接用 require() 函数加载该模块,然后输出当前 LuaSocket 的版本信息。

local socket = require("socket")
print(socket._VERSION)

简单读取一个网站的网页。

#!/usr/bin/lua
local socket = require("socket")
local host, file = "www.baidu.com", "/"

-- 创建一个TCP连接,连接到HTTP连接的标准80端口上
local sock = assert(socket.connect(host, 80))
sock:send("GET " .. file .. " HTTP/1.0\r\n\r\n")
repeat
    -- 以1K的字节块来接收数据,并把接收到字节块输出来
    local chunk, status, partial = sock:receive(2^10)
    io.write(chunk or partial)
until status == "closed"
-- 关闭 TCP 连接
sock:close()

参考