Lua 入门
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 | print(type("Hello world")) --> 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
6print(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
3print("2" + 6) --> 8
print("2" + "6") --> 8
print("2 + 6") --> 2 + 6 -
字符串的连接使用的是 …,如:
1
2print("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
10function 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
12function 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
7co = 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 | -- 初始化表 |
table 的操作
-
table.concat (table [, sep [, i [, j]]]): 连接数组的元素,参数 sep 为分隔符,参数 i 和 j 为连接的范围。
1
2
3
4fruits = {"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
3fruits = {"banana","orange","apple"}
table.insert(fruits,"mango")
print("索引为 4 的元素为 ",fruits[4]) -- mango -
table.remove (table [, pos]): 返回 table 中被删除的元素,pos 为被删除元素的位置,默认为数组部分的末尾。
1
2
3
4fruits = {"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
10fruits = {"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
3for 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
9table = {"apple", "pear", "orange", "grape"}
for key, value in ipairs(table) do
print(key, value)
end
-- 1 apple
-- 2 pear
-- 3 orange
-- 4 grape1
2
3
4
5
6
7table = {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
9table = {"apple", "pear", "orange", "grape"}
for i = 1, #table do
print(i, table[i])
end
-- 1 apple
-- 2 pear
-- 3 orange
-- 4 grape1
2
3
4
5
6
7table = {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 | __add(a, b) --加法 |
Lua的表元素查找机制
很多人对Lua中的元表和元方法都会有一个这样的误解:“如果A的元表是B,那么如果访问了一个A中不存在的成员,就会访问查找B中有没有这个成员”。如果说这样去理解的话,就大错特错了,实际上即使将A的元表设置为B,而且B中也确实有这个成员,返回结果仍然会是nil,原因就是B的__index
元方法没有赋值。别忘了我们之前说过的:“元表是一个操作指南”,定义了元表,只是有了操作指南,但不应该在操作指南里面去查找元素,而__index
方法则是“操作指南”的“索引失败时该怎么办”。下面我们通过几段实际的代码来看一下Lua的表元素的查找过程以便更深入地体会上述这些概念。
1 | father = { |
输出的结果是nil,但如果把代码改为
1 | father = { |
输出的结果为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个步骤:
-
在表中查找,如果找到,返回该元素,找不到则继续。
-
判断该表是否有元表(操作指南),如果没有元表,返回nil,有元表则继续。
-
判断元表(操作指南)中有没有关于索引失败的指南(即
__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 | -- 把tb表中的a和b相加程序 |
也可以换一种写法,只需要传递一个参数就可以
1 | -- 传递一个参数实现tb表中两数相加 |
当我们不想传递参数时可以通过冒号来定义和调用函数,这种就和上面把tb传递过去的做法类似了,但是这里时通过定义函数时隐式把自身传递过去的,当然也可以点与冒号搭配使用,进行指定传递过去的表。
1 | local tb = {a=0,b=1} |
当基本了解table的一些操作后,下面这是一个Lua中的一个简单的类的实现;
1 | -- Class.lua |
Lua 的继承
在 Lua 中是通过 table 以及元表进行实现面向对象思想中的继承,主要通过 setmetatable
以及 __index
两个字段进行实现的,因为当我们为一个表使用 setmetatable
设置了元表后,如果在当前表中找不到该变量或者函数时就会根据 __index
这个字段所包含的地方去找(若 __index
包含的是一个表就会去所包含的表中遍历是否存在该变量,如果 __index
包含的是一个函数则会调用该函数);这就与我们面向对象思想中的基类与派生类的关系相似了,在面向对象语言像C++中若在派生类找不到的变量或函数就会去到基类中去查找。
以下是一个简单的继承的例子:
1 | require "Class" -- 引入Class.lua文件 |
能够成为 “类” 的表和普通的表最大的区别就在于设定了
__index
元方法,这样就相当于赋予了该表 “被继承” 的能力;当该表被设置为元表后,如果在子表中找不到某个变量或者函数时就会根据元表中的__index
所指向的表去查找;因此创建新对象的方式都是通过将创建的新表的元表设置为该类的表,这样就相当于创建了一个新的对象,虽然该对象中没有该类的变量或者函数,但是通过元表的__index
方法就可以在元表中找到了。
但是与C++这些面向对象(部分特性)语言相比较而言;就没有那么好用;并且在使用时需要注意基类与派生类之间的关系,有时候会因为不注意而导致内存访问时出现混乱;一般在使用时尽量不同的操作要独立为一个函数,否则有可能会出现派生类操作的都是基类的变量的情况,例如在下面程序中把 self.name = newname
放到了 new 函数内,就导致了每次重新创建一个对象后对它的 name
进行初始值后,之前创建的对象 cat 中的 name 的值也会随之改变;实际这里操作的一直都是基类中的变量,而非自身的变量。
1 | animal = {name="default"} |
在上述程序中,这种继承将会导致多个派生类访问的都是基类中的变量;导致这种情况的原因是因为在new一个对象时,每次的self指针都是animal这个table里面的变量;我们想要解决这种情况需要重新定义一个函数专门用于初始化name这个变量的,这样每次调用时因为时冒号调用的就会把自身传递过去,当设置名字时如果自己没有name这个变量就会重新分配内存进行赋值操作了;这样就能解决多个派生类共用基类内存中的变量了(如果不理解这段话的可以直接看代码)
1 | animal = {name="default"} |
Lua 的多态
多态是面向对象的三大特性之一,多态性是指允许不同类的对象对同一消息作出响应。在Lua中,因为无论如何前面的函数都会被最后一个同名函数所覆盖;同理派生类的同名函数也会覆盖基类中的同名函数(所以派生类中重载了基类中的同名函数后调用时就会调用派生类中的函数),所以在Lua中实现多态性就比较简单了,只需要在派生类中重载基类中的函数即可。
如同上述的例子一样,我们在 SubClass
中重载了 print
函数,这样在调用 print
函数时就会调用 SubClass
中的 print
函数了。
1 | require "Class" -- 引入Class.lua文件 |