Lua 数据类型

Lua 是动态类型语言,变量不要类型定义,只需要为变量赋值。 值可以存储在变量中,作为参数传递或结果返回。

Lua 中有 8 个基本类型分别为:nil、boolean、number、string、userdata、function、thread 和 table。

数据类型 描述
nil 这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)。
boolean 包含两个值:false和true。
number 表示双精度类型的实浮点数
string 字符串由一对双引号或单引号来表示
function 由 C 或 Lua 编写的函数
userdata 表示任意存储在变量中的C数据结构
thread 表示执行的独立线路,用于执行协同程序
table Lua 中的表(table)其实是一个"关联数组"(associative arrays),数组的索引可以是数字、字符串或表类型。在 Lua 里,table 的创建是通过"构造表达式"来完成,最简单构造表达式是{},用来创建一个空表。

我们可以使用 type 函数测试给定变量或者值的类型:

1
2
3
4
5
6
7
print(type("Hello world"))      --> string
print(type(10.4*3)) --> number
print(type(print)) --> function
print(type(type)) --> function
print(type(true)) --> boolean
print(type(nil)) --> nil
print(type(type(X))) --> string

注意,对于所有的数据类型,type 的返回结果均为一个 string,只是 string 的内容不同。

1
type(type(X))==string

nil

  • nil 类型表示一种没有任何有效值,它只有一个值 – nil,例如打印一个没有赋值的变量,便会输出一个 nil 值。

  • 对于全局变量和 table,nil 还有一个"删除"作用,给全局变量或者 table 表里的变量赋一个 nil 值,等同于把它们删掉。

  • nil 作比较时应该加上双引号 "

    1
    2
    3
    4
    5
    6
    7
    > type(X)
    nil
    > type(X)==nil
    false
    > type(X)=="nil"
    true
    >

    type(X)==nil 结果为 false 的原因是 type(X) 实质是返回的 “nil” 字符串,是一个 string 类型。

boolean

  • Lua 把 false 和 nil 看作是 false,其他的都为 true,数字 0 也是 true。

number

  • Lua 默认只有一种 number 类型 – double(双精度)类型(默认类型可以修改 luaconf.h 里的定义),以下几种写法都被看作是 number

    1
    2
    3
    4
    5
    6
    print(type(2))
    print(type(2.2))
    print(type(0.2))
    print(type(2e+1))
    print(type(0.2e-1))
    print(type(7.8263692594256e-06))

string

  • 字符串由一对双引号或单引号来表示, 也可以用 2 个方括号 “[[]]” 来表示"一块"字符串。

  • 在对一个数字字符串上进行算术操作时,Lua 会尝试将这个数字字符串转成一个数字:

    1
    2
    3
    print("2" + 6)  --> 8
    print("2" + "6") --> 8
    print("2 + 6") --> 2 + 6
  • 字符串的连接使用的是 …,如:

    1
    2
    print("a" .. 'b')   --> ab
    print(0 .. 1) --> 01
  • 使用 # 来计算字符串的长度,放在字符串前面,如:

    1
    print(#"hello")     --> 5

function

  • 在 Lua 中,函数是被看作是"第一类值(First-Class Value)",函数可以存在变量里:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function factorial1(n)
    if n == 0 then
    return 1
    else
    return n * factorial1(n - 1)
    end
    end

    fact = factorial1
    print(fact(5))
  • 函数可以以匿名函数(anonymous function)的方式通过参数传递:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function testFun(tab,fun)
    for k , v in pairs(tab) do
    print(fun(k,v));
    end
    end

    tab = {key1 = "val1", key2 = "val2"};
    testFun(tab,
    function(key,val) -- 匿名函数
    return key.."="..val;
    end
    );

thread

  • 线程是程序的执行实体,线程可以独立运行,线程可以与其他线程共享全局变量和其他大部分 Lua 数据。

  • Lua 线程被创建于 Lua 中,通过 coroutine 库可以创建和管理线程。

    1
    2
    3
    4
    5
    6
    7
    co = coroutine.create(
    function(i)
    print(i);
    end
    )
    coroutine.resume(co, 1) --> 1
    print(coroutine.status(co)) --> dead

userdata

  • userdata 是一种用户自定义的类型,用于表示一种由应用程序或 C/C++ 语言库所创建的类型,可以将任意 C 数据存储到 Lua 变量中。

Lua Table

table 是 Lua 的一种数据结构用来帮助我们创建不同的数据类型,如:数组、字典等。

Lua table 使用关联型数组,你可以用任意类型的值来作数组的索引,但这个值不能是 nil。

Lua table 是不固定大小的,你可以根据自己需要进行扩容。

Lua也是通过table来解决模块(module)、包(package)和对象(Object)的。 例如string.format表示使用"format"来索引table string。

table 的构造

构造器是创建和初始化表的表达式。表是Lua特有的功能强大的东西。最简单的构造函数是{},用来创建一个空表。可以直接初始化数组:

1
2
3
4
5
6
7
8
9
-- 初始化表
mytable = {}

-- 指定值
mytable[1]= "Lua"

-- 移除引用
mytable = nil
-- lua 垃圾回收会释放内存

table 的操作

  • table.concat (table [, sep [, i [, j]]]): 连接数组的元素,参数 sep 为分隔符,参数 i 和 j 为连接的范围。

    1
    2
    3
    4
    fruits = {"banana","orange","apple"}
    print("连接后的字符串 ",table.concat(fruits)) -- bananaorangeapple
    print("连接后的字符串 ",table.concat(fruits,", ")) -- banana, orange, apple
    print("连接后的字符串 ",table.concat(fruits,", ", 2, 3)) -- orange, apple
  • table.insert (table, [pos,] value): 在 table 的数组部分指定位置 pos 插入值为 value 的元素。pos 参数可选,默认为数组部分末尾。

    1
    2
    3
    fruits = {"banana","orange","apple"}
    table.insert(fruits,"mango")
    print("索引为 4 的元素为 ",fruits[4]) -- mango
  • table.remove (table [, pos]): 返回 table 中被删除的元素,pos 为被删除元素的位置,默认为数组部分的末尾。

    1
    2
    3
    4
    fruits = {"banana","orange","apple","mango"}
    print("移除前最后一个元素 ",fruits[4]) -- mango
    table.remove(fruits)
    print("移除后最后一个元素 ",fruits[4]) -- nil
  • table.sort (table [, comp]): 对给定 table 进行升序排序。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    fruits = {"banana","orange","apple","mango"}
    table.sort(fruits)
    for k, v in ipairs(fruits) do
    print(k, v)
    end

    -- 1 apple
    -- 2 banana
    -- 3 mango
    -- 4 orange

table 的遍历

  • pairs: pairs 根据 table 中 key 的 hash 值排列的顺序来遍历,并非创建 table 时各个元素的顺序

    1
    2
    3
    for key, value in pairs(t) do
    print(key, value)
    end
  • ipairs: ipairs 会从 table 数组(lua的hash表和数组都是用的table)下标1开始,一直遍历到key不连续为止(即下标非数字)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    table = {"apple", "pear", "orange", "grape"}
    for key, value in ipairs(table) do
    print(key, value)
    end

    -- 1 apple
    -- 2 pear
    -- 3 orange
    -- 4 grape
    1
    2
    3
    4
    5
    6
    7
    table = {1, 2, key1 = "value1", key2 = "value2"}
    for key, value in ipairs(table) do
    print(key, value)
    end

    -- 1 1
    -- 2 2
  • i = 1, #table 遍历

    这种遍历依赖于 #table 获取到的长度,通常建议用于数组型 table,且下标连续。否则可能表现不一致或者容易取出nil值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    table = {"apple", "pear", "orange", "grape"}
    for i = 1, #table do
    print(i, table[i])
    end

    -- 1 apple
    -- 2 pear
    -- 3 orange
    -- 4 grape
    1
    2
    3
    4
    5
    6
    7
    table = {1, 2, key1 = "value1", key2 = "value2"}
    for i = 1, #table do
    print(i, table[i])
    end

    -- 1 1
    -- 2 2

meta table

在 Lua table 中我们可以访问对应的 key 来得到 value 值,但是却无法对两个 table 进行操作。因此 Lua 提供了元表(Metatable),允许我们改变 table 的行为,每个行为关联了对应的元方法。通俗来说,元表就像是一个“操作指南”,里面包含了一系列操作的解决方案,例如__index 方法就是定义了这个表在索引失败的情况下该怎么办,__add 方法就是告诉table在相加的时候应该怎么做。这里面的__index__add就是元方法。

meta method

通过上面的知识,我们知道了通过使用元表可以定义Lua如何计算两个table的相加操作。当Lua试图对两个表进行相加时,先检查两者之一是否有元表,之后检查是否有一个叫 __add 的字段,若找到,则调用对应的值。__add等即时字段,其对应的值(往往是一个函数或是table)就是"元方法"。

下面是一些Lua表中可以重新定义的元方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
__add(a, b) --加法
__sub(a, b) --减法
__mul(a, b) --乘法
__div(a, b) --除法
__mod(a, b) --取模
__pow(a, b) --乘幂
__unm(a) --相反数
__concat(a, b) --连接
__len(a) --长度
__eq(a, b) --相等
__lt(a, b) --小于
__le(a, b) --小于等于
__index(a, b) --索引查询
__newindex(a, b, c) --索引更新
__call(a, ...) --执行方法调用
__tostring(a) --字符串输出
__metatable --保护元表

Lua的表元素查找机制

很多人对Lua中的元表和元方法都会有一个这样的误解:“如果A的元表是B,那么如果访问了一个A中不存在的成员,就会访问查找B中有没有这个成员”。如果说这样去理解的话,就大错特错了,实际上即使将A的元表设置为B,而且B中也确实有这个成员,返回结果仍然会是nil,原因就是B的__index元方法没有赋值。别忘了我们之前说过的:“元表是一个操作指南”,定义了元表,只是有了操作指南,但不应该在操作指南里面去查找元素,而__index方法则是“操作指南”的“索引失败时该怎么办”。下面我们通过几段实际的代码来看一下Lua的表元素的查找过程以便更深入地体会上述这些概念。

1
2
3
4
5
6
7
8
father = {
house=1
}
son = {
car=1
}
setmetatable(son, father) --把son的metatable设置为father
print(son.house) --nil

输出的结果是nil,但如果把代码改为

1
2
3
4
5
6
7
8
9
father = {
house=1
}
father.__index = father --设置元表的__index为元表自身
son = {
car=1
}
setmetatable(son, father) --把son的metatable设置为father
print(son.house) --1

输出的结果为1,符合预期。

这样一来,结合上例,来解释__index元方法的含义:

在上述例子中,访问son.house时,son中没有house这个成员,但Lua接着发现son有元表father,注意:此时,Lua并不是直接在father中找名为house的成员,而是调用father的__index方法,如果__index方法为nil,则返回nil,如果是一个表(上例中father的__index方法等于自己,就是这种情况),那么就到__index方法所指的这个表中查找名为house的成员,于是,最终找到了house成员。

注:__index 方法除了可以是一个表,还可以是一个函数,如果是一个函数,__index 方法被调用时将返回该函数的返回值。

到这里,总结一下Lua查找一个表元素时的规则,其实就是如下3个步骤:

  1. 在表中查找,如果找到,返回该元素,找不到则继续。

  2. 判断该表是否有元表(操作指南),如果没有元表,返回nil,有元表则继续。

  3. 判断元表(操作指南)中有没有关于索引失败的指南(即 __index 方法),如果没有(即 __index 方法为 nil),则返回nil;如果__index 方法是一个表,则重复1、2、3;如果 __index 方法是一个函数,则返回该函数的返回值。

Lua 面向对象

Lua 是一门面向过程(procedure-oriented)与函数式编程(functional programming)的语言,因为 Lua 它是定位于开发中小型程序,往往不会用于编写大型程序;所以它并没有提供面向对象思想的,很多都是通过模拟出来的;这里的关键就是元表和元方法,通过元表以及元方法就可以模拟出一些面向对象语言中的行为或者思想。

Lua 的类

一个类就是创建对象的模具,对象又是某个特定类的实例;在Lua中table可以有属性(成员变量),也可以有成员方法(通过table+function实现);因此可以通过table来描述对象的属性。

首先这里讲一下关于Lua中定义函数时“.”和“:”的区别,在Lua中也有类似于C++中的this指针,在Lua中是self,当我们定义函数时使用的是冒号,就相当于隐式传递了一个当前调用者过去。

1
2
3
4
5
6
7
-- 把tb表中的a和b相加程序
local tb = {a=0,b=1}
function tb.add(a,b)
return a+b
end

print(tb.add(tb.a,tb.b)) --1

也可以换一种写法,只需要传递一个参数就可以

1
2
3
4
5
6
7
-- 传递一个参数实现tb表中两数相加
local tb = {a=0,b=1}
function tb.add(self)
return self.a+self.b
end

print(tb.add(tb))

当我们不想传递参数时可以通过冒号来定义和调用函数,这种就和上面把tb传递过去的做法类似了,但是这里时通过定义函数时隐式把自身传递过去的,当然也可以点与冒号搭配使用,进行指定传递过去的表。

1
2
3
4
5
6
7
8
local tb = {a=0,b=1}
function tb:add()
return self.a+self.b
end

print(tb:add()) -- 需要使用冒号进行调用
--print(tb.add(tb)) -- 一样可行,冒号定义函数其实就是隐式通过第一个参数把自身传递过去
print(tb.add(tb1)) -- 也可以不通过冒号进行调用,使用点进行调用可指定传过去的表

当基本了解table的一些操作后,下面这是一个Lua中的一个简单的类的实现;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
-- Class.lua

Class = {
x = 0,
y = 0
}
Class.__index = Class -- 设置元表的__index为元表自身,这样才能在类中找到对应的方法

function Class:new(x, y)
local obj = {}
setmetatable(obj, Class)
obj.x = x
obj.y = y
return obj
end

function Class:print()
print(self.x, self.y)
end

local obj = Class:new(1, 2)
obj:print() -- : 语法糖, 等价于 obj.print(obj), 默认传入self
-- 1 2
obj.print(obj) -- 二者等价
-- 1 2

Lua 的继承

在 Lua 中是通过 table 以及元表进行实现面向对象思想中的继承,主要通过 setmetatable 以及 __index 两个字段进行实现的,因为当我们为一个表使用 setmetatable 设置了元表后,如果在当前表中找不到该变量或者函数时就会根据 __index 这个字段所包含的地方去找(若 __index 包含的是一个表就会去所包含的表中遍历是否存在该变量,如果 __index 包含的是一个函数则会调用该函数);这就与我们面向对象思想中的基类与派生类的关系相似了,在面向对象语言像C++中若在派生类找不到的变量或函数就会去到基类中去查找。

以下是一个简单的继承的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
require "Class"  -- 引入Class.lua文件

SubClass = {
z = 0
}
SubClass.__index = SubClass
SubClass = setmetatable(SubClass, Class)

function SubClass:new(x, y, z)
local obj = Class:new(x, y)
setmetatable(obj, SubClass)
obj.z = z
return obj
end

function SubClass:print()
print(self.x, self.y, self.z)
end

function SubClass:sum()
return self.x + self.y + self.z
end

能够成为 “类” 的表和普通的表最大的区别就在于设定了 __index 元方法,这样就相当于赋予了该表 “被继承” 的能力;当该表被设置为元表后,如果在子表中找不到某个变量或者函数时就会根据元表中的 __index 所指向的表去查找;因此创建新对象的方式都是通过将创建的新表的元表设置为该类的表,这样就相当于创建了一个新的对象,虽然该对象中没有该类的变量或者函数,但是通过元表的 __index 方法就可以在元表中找到了。

但是与C++这些面向对象(部分特性)语言相比较而言;就没有那么好用;并且在使用时需要注意基类与派生类之间的关系,有时候会因为不注意而导致内存访问时出现混乱;一般在使用时尽量不同的操作要独立为一个函数,否则有可能会出现派生类操作的都是基类的变量的情况,例如在下面程序中把 self.name = newname 放到了 new 函数内,就导致了每次重新创建一个对象后对它的 name 进行初始值后,之前创建的对象 cat 中的 name 的值也会随之改变;实际这里操作的一直都是基类中的变量,而非自身的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
animal = {name="default"}

function animal:new(tb,newname)
tb = tb or {} -----> 若tb为nil将赋值一个空表
setmetatable(tb,self) -----> 设置为元表
self.__index = self

self.name = newname -----> 这里修改的其实是基类的变量

return tb
end

function animal:myprint()
print(self.name)
end

cat = animal:new(nil,"cat")
cat:myprint() -----> 打印cat

dog = animal:new(nil,"dog") -----> 这里修改的其实是基类的变量
dog:myprint() -----> 打印dog

cat:myprint() -----> 打印dog

在上述程序中,这种继承将会导致多个派生类访问的都是基类中的变量;导致这种情况的原因是因为在new一个对象时,每次的self指针都是animal这个table里面的变量;我们想要解决这种情况需要重新定义一个函数专门用于初始化name这个变量的,这样每次调用时因为时冒号调用的就会把自身传递过去,当设置名字时如果自己没有name这个变量就会重新分配内存进行赋值操作了;这样就能解决多个派生类共用基类内存中的变量了(如果不理解这段话的可以直接看代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
animal = {name="default"}

function animal:new(tb)
tb = tb or {} ----->若tb为nil将赋值一个空表
setmetatable(tb,self) --->设置为元表
self.__index = self

-- self.name = newname --这里相当于是animal.name = newname

return tb
end

function animal:myprint()
print(self.name)
end


function animal:setname( name )
self.name = name
end

cat = animal:new(nil)
cat:setname("cat") ---这里cat调用setname函数后,函数内相当于是cat.name = name
cat:myprint()

dog = animal:new(nil)
dog:setname("dog") ---这里dog调用setname函数后,函数内相当于是dog.name = name
dog:myprint()

mouse = animal:new(nil)
mouse:myprint()

dog:myprint() ---此时再打印dog的name不会出现因为前面的修改了

Lua 的多态

多态是面向对象的三大特性之一,多态性是指允许不同类的对象对同一消息作出响应。在Lua中,因为无论如何前面的函数都会被最后一个同名函数所覆盖;同理派生类的同名函数也会覆盖基类中的同名函数(所以派生类中重载了基类中的同名函数后调用时就会调用派生类中的函数),所以在Lua中实现多态性就比较简单了,只需要在派生类中重载基类中的函数即可。

如同上述的例子一样,我们在 SubClass 中重载了 print 函数,这样在调用 print 函数时就会调用 SubClass 中的 print 函数了。

1
2
3
4
5
6
7
require "Class"  -- 引入Class.lua文件

local obj1 = Class:new(1, 2)
obj1:print() -- 1 2

local obj2 = SubClass:new(1, 2, 3)
obj2:print() -- 1 2 3