聊一聊 Lua 的基础数据类型:数值、布尔、字符串

发布时间 2023-06-26 18:59:29作者: 古明地盆

楔子

任何一门语言都提供了不同类型的数据结构,那么 Lua 中都有哪些数据结构呢?

  • nil:空
  • boolean:布尔类型,分别是 true 和 false
  • number:数值类型,整型和浮点型都属于 number
  • string:字符串
  • table:表
  • function:函数
  • userdata:用户数据
  • thread:线程

Lua总共提供了以上 8 种数据类型,目前只需要知道一下即可,我们先将一些基础概念等前置工作说一下,后面会一点一点地慢慢介绍。

然后是 Lua 的关键字,总共有 22 个:

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

这些关键字不用刻意去记,因为还是那句话,既然学习 Lua,肯定有其它编程语言基础,所以这些关键字显然是大部分都见过的。至于不认识的关键字,我们后面也会慢慢遇到。

最后再补充一下 Lua 的注释,Lua 里面也分为单行注释和多行注释。单行注释和 SQL 一样,使用两个减号:

-- 这是单行注释

多行注释以 --[[ 开头,以 ]] 结尾,里面写注释。

--[[

 这是多行注释
 并且开头的 -- 和 [[ 之间不可以有空格,结尾是两个 ]
 
]]

下面这种写法也是多行注释,不是两行单行注释。

--[[

 这也是多行注释
 不是两行单行注释
 
--]]

下面我们就来聊一聊 Lua 的基本类型。

数值类型

Lua 中的数值类型为 number,整型和浮点型都是 number。

-- Lua 和 Python 类似,在创建变量时不需要指定类型
-- 解释器会自动根据赋的值来判断
a = 123
b = 3.14
print(a, b)  -- 123	3.14

-- Lua中,每一行语句的结尾也不需要加分号,直接换行即可
-- 当然加分号也是可以的,跟 Python 是类似的
c = 123;
d = 456
print(c, d)  -- 123	456

-- 并且在 Python 中,如果加上了分号,那么两行赋值可以写一行
-- 比如 e = 1; f = 2
-- 这在 Lua 中也是可以的
e = 1; f = 2
print(e, f)  -- 1	2

-- 但是 Lua 比较彪悍的是,不加分号也可以
-- 如果在 Python 中这么写,则肯定是报错的
g = 3 h = 4
print(g, h)  -- 3 4

-- 但是我们不建议将多行赋值语句写在同一行里面,最好要分行写
-- 当然在 Lua 中也可以使用多元赋值
a, b = 1, 2
print(a, b)  -- 1	2

这里可能有人发现了,我们在最上面已经创建 a 和 b 这两个变量了,但是下面又创建了,这一点和 Python 类似,可以创建多个同名变量。比如创建 a = 1,然后又创建 a = 2,这是允许的。只不过这相当于发生了更新,将 a 的值由 1 变成了 2,当然即便赋值为其它类型也是可以的。

因为在 Lua 中,全局变量是通过 table、也就是"表"来存储的。这个 table 后面会详细说,你暂时可以理解为哈希表,或者当成 Python 中的字典,而且 Python 中全局变量也是通过字典存储的。

补充:数值还可以使用 16 进制表示,比如 0x333。

我们通过 Lua 的数值类型,来演示了如何在 Lua 中如何创建一个变量,并且还介绍了 Lua 中全局变量的存储方式,下面再来看看如何区分整型和浮点型。

a = 123
b = 123.  -- . 后面不写东西的话,默认是 .0
c = .123  -- . 前面不写东西的话,默认是 0.
print(a, b, c)  -- 123	123.0	0.123

-- Lua 中,可以使用 type 函数检测变量的类型
print(type(a))  -- number
print(type(b))  -- number
print(type(c))  -- number

-- 这个 type 是内置的,它检测的是 Lua 中的基础类型
-- 而我们说 Lua 不区分整型和浮点型,如果想精确区分的话,那么可以使用 math.type
-- 整型是 integer,浮点型是 float
print(math.type(a))  -- integer
print(math.type(b))  -- float
print(math.type(c))  -- float
-- 如果一个数值中出现了小数点,那么 math.type 得到的就是 float

使用 type 和 math.type 得到的都是一个字符串,另外我们是直接使用的 math.type,这个 math 是哪里来的。其实 Lua 解释器内置了很多包,我们直接用即可,并且也不需要像 Python 那样使用之前先导入。

整数和浮点数的比较
print(3 == 3.0)  -- true
print(-3 == -3.0)  -- true

-- 我们看到,如果小数点后面是 0,那么也是相等的,这一点和 Python 是一样的
-- Lua 也支持如下方式创建浮点数
print(3e3)  -- 3000.0

-- 但是我们看到,得到是浮点
-- 在 Lua 中,只要十进制数中出现了小数点、或者出现了幂运算、除法运算,那么得到的都是一个浮点
-- 准确的说,math.type 检测的结果是 float,因为 Lua 中不区分整型和浮点,它们都是 number
-- 这里我们说浮点只是为了方便,理解意思即可

-- Lua中 a ^ b 表示 a 的 b 次方
-- 如果运算中出现了浮点数,或者发生了幂运算,那么结果就是浮点
print(3 ^ 2)  -- 9.0
print(3 * 3)  -- 9

在 Lua 中,只要十进制数中出现了小数点、或者出现了幂运算,那么得到的都是一个浮点数。准确的说,math.type 检测的结果是 float,因为 Lua 中不区分整数和浮点数,它们都是 number。

算术运算符

算数运算没啥好说的,总共有 8 个。

  • +(相加)
  • -(相减)
  • *(相乘)
  • /(相除)
  • //(地板除)
  • %(取模)
  • ^(幂运算)
  • ~(取反)

对于相加、相减、相乘、取模,如果是两个整型,那么结果还是整型,如果出现了浮点,那么结果为浮点。

print(1 + 2, 1 + 2.)  -- 3	3.0
print(1 * 2, 1 * 2.)  -- 2	2.0
print(1 - 2, 1 - 2.)  -- -1	-1.0
print(13 % 5, 13. % 5)  -- 3	3.0

如果相除,那么结果一定为浮点。

print(3 / 2, 4 / 2, 4 / 2.)  -- 1.5	2.0	2.0

当然在 Lua 中,还有一个地板除,会对商向下取整,这是在 Lua5.3 中引入的。

print(3 // 2, 4 // 2)  -- 1	2

-- 如果里面出现了浮点,那么即使是地板除,也一样会得到小数
print(4 // 2.)  -- 2.0
-- 虽然是浮点,但我们看到的是 1.0, 相当于还是有向下取整的效果
print(4 // 2.5)  -- 1.0

当然 Lua 中还有幂运算,使用 ^ 表示。

print(3 ^ 4)  -- 81.0
print(3e4)  -- 30000.0

只要出现了幂运算,得到的一定是浮点。最后是取反,使用波浪号:

print(~8)  -- -9
print(~-7)  -- -6

关于整数取反, 有一个快速记忆的方式:对整数 x 的取反结果为 (x + 1) * -1。

按位运算符

按位运算符有 5 个,分别是:

  • &(按位与)
  • |(按位或)
  • ~(按位异或、取反)
  • <<(左移)
  • >>(右移)
-- 按位与 &
print(15 & 20)  -- 4
-- 按位或 |
print(15 | 20)  -- 31
-- 按位异或 ~,异或在 Python 中是 ^,但 ^ 在 Lua 是幂运算
print(15 ~ 20)  -- 27
-- 取反,取反的话也是 ~
print(~20)  --  -21

-- 左移
print(2 << 3)  -- 16
-- 右移
print(16 >> 2)  -- 4

以上这些操作符是在 5.3 当中才提供的,如果是之前的版本,则不能使用这些操作符。

比较运算符

比较运算符又叫关系(Relational)运算符,一共有 6 个,分别是:

  • ==(等于)
  • ~=(不等于)
  • >(大于)
  • >=(大于等于)
  • <(小于)
  • <=(小于等于)

这个比较简单,没什么好说的,但注意:不等于在 Lua 里面是 ~=,而非 !=。

逻辑运算符

逻辑运算符有 3 个,分别是:and(逻辑与)、or(逻辑或)、not(逻辑非)。Lua 语言没有给逻辑运算符分配专门的符号,而是给它们分配了三个关键字,这一点和 Python 是一样的。

需要注意的是,除了 false 和 nil 会被视为假之外,其它值一律为真。

数学库

Lua 也提供了一个数学库,叫做 math,除了可以用它查看数值的具体类型,里面还定义了很多用于数学计算的高级函数,比如:sin、cos、tan、asin、floor、ceil 等等。

print(math.sin(math.pi / 6))  -- 0.5

比较简单,更多的函数可以自己查看。

布尔类型

再来看一下 Lua 的布尔类型,该类型只有两个值:true 和 false。除了 nil 和 false 的布尔值为 false 之外,其它值的布尔值均为 true。那么如何得到一个布尔值呢?

-- 直接创建布尔值
flag1 = true
flag2 = false
print(flag1, flag2)  -- true false

-- 将某个值转成布尔值
num = 0
print(not not num) -- true

-- 调用两次 not 即可得到布尔值,这在 Python 中也是可以的
-- 只不过在 Python 里面还可以使用内置的 bool 函数,但 Lua 里面则没有这个函数

再次强调:像 0、空字符串等,在 Lua 里面会被视为真值,只有 nil 和 false 为假,其它均为真。

字符串

下面我们看看 Lua 的字符串,字符串既可以使用双引号、也可以使用单引号。注意:Lua 的字符串是不可变对象,不能本地修改,只能创建新的字符串。

name = "komeiji satori"
print(name) -- komeiji satori

-- 使用 # 可以获取其长度
print(#name, #"name") -- 14	4

-- 使用 .. 可以将两个字符串连接起来
print("aa" .. "bb") -- aabb
print("name: " .. name) -- name: komeiji satori

-- .. 的两边可以没有空格,但是为了规范,建议前后保留一个空格
-- .. 前后还可以跟数字,会将数字转成字符串
print("abc" .. 3, 3 .. 4, 3 .. "abc") -- abc3	34	3abc
-- 另外如果 .. 的前面是数字的话,那么 .. 的前面必须有空格
-- 也就是写成类似于 3 .. 的形式  不可以写 3..
-- 因为 3 后面如果直接出现了. 那么这个 . 会被当成小数点来解释

Lua 内部也支持多行字符串,使用[[]]表示。

msg = [[
    你好呀
    你在什么地方呀
    你吃了吗
]]

print(msg)
--[[
    你好呀
    你在什么地方呀
    你吃了吗

]]
字符串和数值的转换

Lua 的字符串可以和数值相加,也可以相互转换。

print("10" + 2)  -- 12.0

-- 如果字符串和整数运算,那么得到的也是浮点
-- 你可以认为只有整数和整数运算才有可能得到整数,而字符串不是整数

-- 调用 tonumber 可以将字符串显式的转为整数
print(type(tonumber("10")))  -- number
print(tonumber("10") + 2)  -- 12

-- 如果转化失败,那么结果为 nil
print(tonumber("ff"))  -- nil

-- 当然有些时候我们的数字未必是 10 进制,比如上面的 ff,它可以是 16 进制
-- 如果需要进制,那么就给 tonumber 多传递一个参数即可
print(tonumber("ff", 16))  -- 255
print(tonumber("11101", 2))  -- 29
print(tonumber("777", 8))  -- 511

-- 8 进制,允许出现的最大数是 7,所以转化失败,结果为nil
print(tonumber("778", 8))  -- nil

-- 整型转化成字符串,则是tostring
print(tostring(100) == "100")  -- true
print(tostring(100) == 100)  -- false

我们看到整数(还有浮点数)和字符串是可以相加的,当然相减也可以,会将字符串转成浮点数进行运算。也可以判断是否相等或者不相等,这个时候会根据类型判断,不会隐式转化了,由于两者类型不一样,直接不相等。

但整数和字符串两者是无法比较大小的,只能判断是否相等或者不等,因为 2 < 15 ,但是 "2" > "15"。所以为了避免混淆,在比较的时候 Lua 不会隐式转化、加上类型不同也无法比较大小,因此直接抛异常。

字符串标准库

Lua 处理字符串还可以使用一个叫做 string 的标准库,这个标准库在 5.3 中也是内嵌在解释器里面的,我们直接通过 string.xxx 即可使用。下面就来看看 string 这个标准库都提供了哪些函数吧,这里说一下 Lua 的字符串是以字节为单位,而不是以字符为单位。因此 string 的大部分函数不适合处理中文(也有少数例外),如果是中文,可以使用后面要介绍的 utf8。

-- 查看字符串的长度
print(string.len("abc"), #"abc")  -- 3	3
-- 一个汉字占三个字节,默认是以字节为单位的,计算的是字节的个数
print(string.len("古明地觉"), #"古明地觉")  -- 12	12

-- 重复字符串 n 次
print(string.rep("abc", 3))  -- abcabcabc
-- 如果是单纯的重复字符串的话,也可以对中文操作,因为不涉及字符的局部截取
print(string.rep("古明地觉", 3))  -- 古明地觉古明地觉古明地觉

-- 字符串变小写,可以用于中文,但是没意义
print(string.lower("aBc"))  -- abc
print(string.lower("古明地觉"))  -- 古明地觉

-- 同理还有转大写
print(string.upper("aBc"))  -- ABC
print(string.upper("古明地觉"))  -- 古明地觉

-- 字符串翻转,这个不适合中文
print(string.reverse("abc"))  -- cba
print(string.reverse("古明地觉"))  -- ��谜厘椏�
-- 我们看到中文出现了乱码,原因就是这个翻转是以字节为单位从后向前翻转
-- 而汉字占 3 个字节,需要以 3 个字节为单位翻转


-- 字符串截取,注意:Lua 中索引是从 1 开始的
-- 结尾也可以写成 -1,并且字符串截取包含首位两端
print(string.sub("abcd", 1, -1))  -- abcd
print(string.sub("abcd", 2, -2))  -- bc
-- 可以只指定开头,不指定结尾,但是不可以开头结尾都不指定
print(string.sub("abcd", 2))  -- bcd

-- 同样不适合中文,除非你能准确计算字节的数量
print(string.sub("古明地觉", 1, 3))  -- 古
print(string.sub("古明地觉", 1, 4))  -- 古�
-- 超出范围,就为空字符串
print(string.sub("古明地觉", 100, 400) == "")  -- true


-- 将数字转成字符
print(string.char(97))  -- a
-- 如果是多个数字,那么在转化成字符之后会自动拼接成字符串
print(string.char(97, 98, 99))  -- abc


-- 字符转成数字,默认只转化第1个
print(string.byte("abc"))  -- 97
-- 可以手动指定转化第几个字符
print(string.byte("abc", 2))  -- 98
print(string.byte("abc", -1))  -- 99
-- 超出范围,那么返回 nil
print(string.byte("abc", 10) == nil)   -- nil
-- 转化多个字符也是可以的,这里转化 1 到 -1 之间的所有字符
print(string.byte("abc", 1, -1))  -- 97	98 99
-- 越界也没事,有几个就转化几个
print(string.byte("abc", 1, 10))  -- 97	98 99
-- 另外,这里是返回了多个值,我们也可以用多个变量去接收
a, b, c = string.byte("abc", 1, 10)
print(a, b, c)  -- 97	98 99


-- 关于 Lua 返回值,这涉及到了函数,我们后面会说
-- 字符串的格式化,格式化的风格类似于C
print(
    string.format("name = %s, age = %d, number = %03d", "古明地觉", 17, 1)
)  -- name = 古明地觉, age = 17, number = 001

-- 字符串的查找,会返回两个值,分别是开始位置和结束位置
print(string.find("abcdef", "de"))  -- 4 5
-- 不存在则为 nil
print(string.find("abcdef", "xx"))  -- nil


-- 字符串的全局替换,这个替换可以用中文
-- 返回替换之后的字符串和替换的个数
print(string.gsub("古名地觉 名名 那么可爱", "名", "明"))  -- 古明地觉 明明 那么可爱	3
-- 我们同样可以使用返回值去接
new_str, count = string.gsub("古名地觉 名名 那么可爱", "名", "明")
print(new_str)  -- 古明地觉 明明 那么可爱

关于处理 ASCII 字符,string 库为我们提供了以上的支持,不难发现支持的东西还是比较少的,因为 Lua 的源码总共才两万两千多行,所以这就决定了它没办法提供过多的功能。Lua主要还是用来和别的语言结合的,然而事实上,string 库提供的东西也不少了。

下面来看看 utf8,我们说 string 库不是用来处理 unicode 字符的,如果处理 unicode 字符的话,需要使用 utf8 这个库

-- Lua 中存储 unicode 字符使用的编码是 utf-8
-- 计算长度
print(utf8.len("古明地觉"))  -- 4

-- 类似于 string.byte,这两个可以通用
print(utf8.codepoint("古明地觉", 1, -1))  -- 21476	26126	22320	35273

-- 类似于 string.char,这两个可以通用
print(utf8.char(21476, 26126, 22320, 35273))  -- 古明地觉

-- 截取,使用 string.sub
-- 但使用 utf-8 编码,不同字符占的字节大小可能不一样,这时候怎么截取呢
-- 可以先通过 utf8.offset 计算出,偏移到第 n 个字符的字节量
print(string.sub("古明地觉", utf8.offset("古明地觉", 2)))  -- 明地觉
print(string.sub("古明地觉", utf8.offset("古明地觉", -2)))  -- 地觉



-- 遍历,遍历使用了 for 循环,我们后面说,现在先看一下
for i, c in utf8.codes("古a明b地c觉") do
    print(i, c, utf8.char(c))
    --[[
        1	21476	古
        4	97	    a
        5	26126	明
        8	98	    b
        9	22320	地
        12	99	    c
        13	35273	觉
    ]]
end

以上便是 Lua 处理字符串的一些操作, 尽管功能提供的不是非常的全面,但这与 Lua 的定位有关。

到目前为止,我们就介绍了 Lua 的数值、布尔值和字符串,现在使用的层面了解它,后面我们会分析它的底层实现。