Lua程序数据类型及其使用

来源:互联网 发布:ape flac 知乎 编辑:程序博客网 时间:2024/05/14 08:49

 1、基本数据类型

可以用type()函数来检查一个变量是什么类型。

八种基本类型:
1)数值(number)
数值,在Lua里,数值相当于C语言的double,内部以double表示
2)字符串(string)
总是以零结尾,但可以包含任意字符(包括零,可以包含'\0'字符的),因此并不等价于C字符串,而是其超集。
3)布尔(boolean)
布尔值,只有“true”或者“false”两个值。
4)函数(function)
函数也是一种类型,所有的函数,它本身就是一个变量。
5)表(table)
异构的Hash表。
6)userdata
用户(非脚本用户)定义的C数据结构。脚本用户只能使用它,不能定义。

这个类型专门用来和Lua的宿主打交道的。宿主通常是用C和C++来编写的,在这种情况下,Userdata可以是宿主的任意数据类型,常用的有Struct和指针。
7)线程(thread)
Lua协作线程(coroutine),与一般操作系统的抢占式线程不一样。

在Lua中没有真正的线程。Lua中可以将一个函数分成几部份运行。如果感兴趣的话,可以去看看Lua的文档。
8)nil
代表什么也没有,可以与C的NULL作类比,但它不是空指针。

2、表达式

表达式就是算式概念的扩展。
数学表达式的运算符有 +, -, (乘), (除), (指数),- (负号)
如果操作数是字符串类型,并可以被转换成数值,则将自动转换成数值

关系运算符返回布尔值true或false,有:== (等于),~= (不等于),< (小于),> (大于),<= (小于等于),>= (大于等于)
注意,对数值类型不要用==比较,应判断两数的差是否小于一个足够小的数,否则对有些数看起来相等却不相等。0 除外。

逻辑运算符包括:and (和,并且), or (或者),not (非,不)
逻辑运算的操作数一定要是布尔类型或nil,nil表示空,被当作false处理。使用其他类型的操作数不会报错,因为有其特定的功能,但坚决不推荐使用。

字符串连接运算符:.. (取得两个字符串连接后的字符串)
如果操作数是数值型,则自动转换为字符串

优先级,从高到

^

not   (负号)

    /

    -  

..

        <=    >=    ~=    ==

and
or  

..和^是右结合,就是说 2^2^3 == 2^(2^3)。


hp 10 and not player.frozen -- hp小于10 并且 玩家没有被冰冻时为真

3、流程控制语句

while 表达式 do
  语句块
end
如果表达式的结果为true,则执行语句块,到end后返回while再循环。如果表达式的结果为false,则跳过语句块执行end后的语句。


repeat
  语句块
until 表达式
先执行语句块,再判断表达式是否为true,是则继续向下执行,否则返回repeat重复。


if 表达式 then
  语句块
else
  语句块
end


if 表达式 then
  语句块
elseif 表达式 then
  语句块
else
  语句块
end
根据表达式的结构选择执行。


循环内可用break语句跳出包含该break的一层循环。

for 循环变量 初值, 终值 do
  语句块
end


for 循环变量 初值, 终值,每次的增量 do
  语句块
end


绝对不要在for语句块中修改循环变量。

4、函数

函数即可被重复调用的语句块。
function 函数名(参数列表)
  语句块
end 
参数列表为用逗号分隔的变量名列表,当执行到函数体内部时,他们就是函数体内的局部变量,并且已用调用时指定的实参赋值。参数列表可以为空。
函数可以返回值给调用者,使用  
    return 返回值
执行return之后将从函数返回调用者的下一条语句继续执行。
例: 
function max(a, b)
  if a > b then
    return a
  else
    return b
  end
end
local c = max(10, 20)   -- c == 20
调用函数时使用 
    函数名(参数列表) 或 函数名()
注意括号必须加上,这样才表示函数调用,执行函数内的语句。
如果不加则表示一个函数类型的值。

请注意,return语言一定要写在end之前。假如你非要在中间放上一句return,那么请写成:do return end。
函数也是变量类型,如:
add = function (a,b) return a+b end
当你重新给add赋值时,它就不再表示这个函数了。你甚至可以赋给add任意数据,包括nil (这样,你就清除了add变量)。

和C语言一样,Lua的函数可以接受可变参数个数,它同样是用"…"来定义的,比如:
function sum (a,b,…)
如果想取得…所代表的参数,可以在函数中访问arg局部变量(表类型)得到。
如 sum(1,2,3,4)
则,在函数中,a = 1, b = 2, arg = {3, 4}
更可贵的是,它可以同时返回多个结果,比如:
function s()
  return 1,2,3,4
end
a,b,c,d = s()  -- 此时,a = 1, b = 2, c = 3, d = 4

注意尽量不要使用多返回值,因为脚本解释器不检查变量的个数是否匹配,多则丢弃,少则置nil,容易倒致错误而不易发现。


表类型可以拥有任意类型的值,包括函数!因此,有一个很强大的特性是,拥有函数的表,哦,我想更恰当的应该说是对象吧。Lua可以使用面向对象编程了。不信?举例如下:
t =
{
 Age = 27
 add = function(self, n) self.Age = self.Age+n end
}
print(t.Age)  -- 27
t.add(t, 10) --相当于 t:add(10) 
print(t.Age)  -- 37

5、表

lua的表是自定义的哈希表。

可以把这个类型看作是一个数组。
只是C语言的数组,只能用正整数来作索引;
在Lua中,你可以用任意类型来作数组的索引,除了nil。
同样,在C语言中,数组的内容只允许一种类型;
在Lua中,你也可以用任意类型的值来作数组的内容。


如数组,以从1开始的整数为键索引:
local arr = {10, 100, 1000} -- arr[1] = 10, arr[2] = 100, arr[3] = 1000

词典,以字符串为索引:
local dict = {"pig" = "猪", "dog" = "狗"} -- dict["pig"] = "猪"

对象,以属性为索引:
local pig = {legs = 4, tail = 1, name = "猪"} -- pig.legs = 4 ……

表成员分开定义:
T1 = {}  -- 定义一个空表
T1[1]=10  -- 然后我们就可以象C语言一样来使用它了。
T1["John"]={Age=27, Gender="Male"}
这一句相当于:
T1["John"]={}  -- 必须先定义成一个表,还记得未定义的变量是nil类型吗
T1["John"]["Age"]=27
T1["John"]["Gender"]="Male"
当表的索引是字符串的时候,我们可以简写成:
T1.John={}
T1.John.Age=27
T1.John.Gender="Male"

T1.John{Age=27, Gender="Male"}

也可以一次性定义:
T1=
{
  10,  -- 相当于 [1] = 10
  [100] = 40,
  John=  -- 如果你原意,你还可以写成:["John"] =
  {
    Age=27,   -- 如果你原意,你还可以写成:["Age"] =27
    Gender=Male   -- 如果你原意,你还可以写成:["Gender"] =Male
  },
  20  -- 相当于 [2] = 20
}

1)所有元素之间,总是用逗号","隔开;
2)所有索引值都需要用"["和"]"括起来;如果是字符串,还可以去掉引号和中括号;
3)如果不写索引,则索引就会被认为是数字,并按顺序自动从1往后编;

(2)表遍历

ipairs 和 pairs 遍历表(使用for)

for 循环变量i, 循环变量v in ipairs(数据表t) do
  语句块
end
i是整数键,v是值
循环将逐次得到 (i = 1, v = t[1]), (i = 2, v = t[2]), ……


for 循环变量k, 循环变量v in pairs(数据表t) do
  语句块
end
k是键,v是值
循环将逐次得到数据表中的每一个键-值对。


用ipairs遍历的表,需要是类似t={1,2,3,4,5,6,7,8,9}
,如果后面有个t[11]=11,那么用ipairs就遍历不到。
原因是table里面分两部分,数组和hash表,t[1],t[2]等连续的是相当于数组部分,
不连续的索引都放hash表里了,顺序不一定。


用pairs遍历表,顺序跟k的哈希值有关。

不要在for语句块中修改循环变量。

数据表函数
table.sort(table [, comp]) -- 将数据表元素按值排序。如果指定comp比较函数,则使用comp函数进行比较。comp应有两个参数,当a<b时返回true。
table.insert(table, [pos,] value) -- 在table的pos索引位置插入value。
table.remove(table [, pos]) -- 删除table中索引为pos的元素,省略pos时为最后一个元素。
table.foreachi(table, f) -- 对table中每一个值,调用f函数,参数为整数索引和元素值
table.foreach(table, f) -- 对table中每一个值,调用f函数,参数为键和值

表使用例子:
a = {}b = {x = 1, ["hello, "] = "world!"}a.astring = "ni, hao!"a[1] = 100a["a table"] = bfunction foo()endfunction bar()enda[foo] = bar--分别穷举表a和bfor k, v in pairs(a) do print(k, "=>", v) endprint("----------------------------")for k, v in pairs(b) do print(k, "=>", v) end

输出结果:
1 => 100
a table => table: 003D7238
astring => ni, hao!
function: 
003DBCE0 => function: 
003DBD00
----------------------------
hello, => world!
x => 1


定义表(Table)的方式
a = {}, b = {…}
访问表的成员
通过“.”或者“[]”运算符来访问表的成员。
注意:表达式a.b等价于a[“b”],但不等价于a[b]

表项的键和值
任何类型的变量,除了nil,都可以做为表项的键。从简单的数值、字符串到复杂的函数、表等等都可以;

同样,任何类型的变量,除了nil,都可以作为表项的值。给一个表项的值赋nil意味着从表中删除这一项,比如令a.b = nil,则把表a中键为“b”的项删除。如果访问一个不存在的表项,其值也是nil,比如有c = a.b,但表a中没有键为“b”的项,则c等于nil。

6、简单对象

其实一个表实例可以当做一个对象,该表实例有一些数据,也可以添加一些函数作为成员函数。

function create(name, id)local obj = { name = name, id = id }function obj:SetName(name)self.name = nameendfunction obj:GetName()return self.nameendfunction obj:SetId(id)self.id = idendfunction obj:GetId()return self.idendreturn objendo1 = create("Sam", 001)print("o1's name:", o1:GetName(), "o1's id:", o1:GetId())o1:SetId(100)o1:SetName("Lucy")print("o1's name:", o1:GetName(), "o1's id:", o1:GetId())


输出结果:
o1's name: Sam o1's id: 1
o1's name: Lucy o1's id: 100


对象工厂模式
如前面代码的create函数用表来表示对象
把对象的数据和方法都放在一张表内,虽然没有隐藏私有成员,但对于简单脚本来说完全可以接受。


成员方法的定义
function obj:method(a1, a2, ...) … end 等价于
function obj.method(self, a1, a2, ...) … end 等价于
obj.method = function (self, a1, a2, ...) … end


成员方法的调用
obj:method(a1, a2, …) 等价于
obj.method(obj, a1, a2, ...)


7、简单继承

简单继承就是增加了成员变量和成员函数

function createRobot(name, id)local obj = { name = name, id = id }function obj:SetName(name)self.name = nameendfunction obj:GetName()return self.nameendfunction obj:GetId()return self.idendreturn objend--增加成员position和其访问接口SetPosition和GetPositionfunction createFootballRobot(name,id, position)local obj = createRobot(name, id)obj.position = "right back"function obj:SetPosition(p)self.position = pendfunction obj:GetPosition()return self.positionendreturn objend

优点:

简单、直观
缺点:
传统、不够动态


8、函数闭包

function createCountdownTimer(second)local ms= second * 1000local function countDown()ms= ms- 1return msendreturn countDownendtimer1 = createCountdownTimer(1)for i = 1, 3 doprint(timer1())endprint("------------")timer2 = createCountdownTimer(1)for i = 1, 3 doprint(timer2())end


输出结果:
999
998
997
------------
999
998
997


9、Upvalue

upvalue

一个函数所使用的定义在它的函数体之外的局部变量(external local variable)称为这个函数的upvalue。


在前面的代码中,函数countDown使用的定义在函数createCountdownTimer中的局部变量ms就是countDown的upvalue,但ms对createCountdownTimer而言只是一个局部变量,不是upvalue。

Upvalue是Lua不同于C/C++的特有属性,需要结合代码仔细体会。


函数闭包

一个函数和它所使用的所有upvalue构成了一个函数闭包。


Lua函数闭包与C函数的比较

Lua函数闭包使函数具有保持它自己的状态的能力,从这个意义上说,可以与带静态局部变量的C函数相类比。

但二者有显著的不同:对Lua来说,函数是一种基本数据类型——代表一种(可执行)对象,可以有自己的状态;但是对带静态局部变量的C函数来说,它并不是C的一种数据类型,更不会产生什么对象实例,它只是一个静态地址的符号名称。

我的理解,补充一点是:C函数是有类型的,只是它本身不是一个类型。有的是地址类型,长度是4个字节。


基于对象的实现方式

function create(name, id)local data= { name = name, id = id }local obj = {}function obj.SetName(name)data.name = nameendfunction obj.GetName()return data.nameendfunction obj.SetId(id)data.id = idendfunction obj.GetId()return data.idendreturn objendo1 = create("Sam", 001)o2 = create("Bob", 007)o1.SetId(100)print("o1's id:", o1.GetId(), "o2's id:", o2.GetId())o2.SetName("Lucy")print("o1's name:", o1.GetName(), "o2's name:", o2.GetName())

输出结果:
o1's id: 100 o2's id: 7
o1's name: Sam o2's name: Lucy

实现方式 

把需要隐藏的成员放在一张表里,把该表作为成员函数的upvalue。


局限性 
基于对象的实现不涉及继承及多态。但另一方面,脚本编程是否需要继承和多态要视情况而定


10、元表

元表定义

元表本身只是一个普通的表,通过特定的方法(比如setmetatable)设置到某个对象上,进而影响这个对象的行为;

一个对象有哪些行为受到元表影响以及这些行为按照何种方式受到影响是受Lua语言约束的。

比如在前面的代码里,两个表对象的加法运算,如果没有元表的干预,就是一种错误;

但是Lua规定了元表可以“重载”对象的加法运算符,因此若把定义了加法运算的元表设置到那两个表上,它们就可以做加法了。


元表是Lua最关键的概念之一,内容也很丰富,请参考Lua文档了解详情。


如果把表比作对象,元表就是可以改变对象行为的“元”对象。

元表与C++虚表的比较:

相同点

可以作为函数查找的依据

不同点

元表可以动态的改变,C++虚表是静态不变的;

元表可以影响表(以及其他类型的对象)的很多方面的行为,虚表主要是为了定位对象的虚方法(最多再带上一点点RTTI)。


(1)设置元表的成员变量

通过设置元表的__index成员来实现
t = {}m = { a = " and ", b = "Li Lei", c = "Han Meimei" }setmetatable(t, { __index= m}) --表{ __index=m }作为表t的元表for k, v in pairs(t) do --穷举表tprint(k, v)endprint("-------------")print(t.b, t.a, t.c)


输出结果:
-------------
Li Lei and Han Meimei


(2)设置元表的成员函数

--setmetatable相当于重载类的方法,lua 里的函数也是类型。function add(t1, t2)--‘#’运算符取表长度assert(#t1 == #t2) local length = #t1for i = 1, length dot1[i] = t1[i] + t2[i]endreturn t1end--setmetatable返回被设置的表t1 = setmetatable({ 1, 2, 3}, { __add = add })t2 = setmetatable({ 10, 20, 30 }, {__add = add })t1 = t1 + t2for i = 1, #t1 doprint(t1[i])end 


输出结果:
11
22
33



11、基于原型的继承

实际上是使用了外部的表(Robot )作为upvalues而存在,然后写了些类似c++的一些面向对象的函数和操作符new。

FootballRobot作为一个继承类(实际也是个元表)

Robot原来没有的成员,通过设置另一个元表({position = "right back"})来实现,再加上FootballRobot的新函数SetPosition和GetPosition,实现FootballRobot。

个人觉得这样设置元表跟面向对象的封装性需求不合。

Robot = { name = "Sam", id = 001 }function Robot:New(extension)local t = setmetatable(extension or { }, self)self.__index = self -- 继承类可以设置Robot为元表而访问Robot的成员return tendfunction Robot:SetName(name)self.name = nameendfunction Robot:GetName()return self.nameendfunction Robot:SetId(id)self.id = idendfunction Robot:GetId()return self.idendrobot = Robot:New()print("robot's name:", robot:GetName())print("robot's id:", robot:GetId())print("-----------------")FootballRobot = Robot:New({position = "right back"})function FootballRobot:SetPosition(p)self.position = pendfunction FootballRobot:GetPosition()return self.positionendfr = FootballRobot:New()print("fr's position:", fr:GetPosition())print("fr's name:", fr:GetName())print("fr's id:", fr:GetId())print("-----------------")fr:SetName("Bob")print("fr's name:", fr:GetName())print("robot's name:", robot:GetName())

输出结果:

robot's name: Sam
robot's id: 1
-----------------
fr's position: right back
fr's name: Sam
fr's id: 1
-----------------
fr's name: Bob
robot's name: Sam


12、prototype模式

一个对象既是一个普通的对象,同时也可以作为创建其他对象的原型的对象(即类对象,class object);

动态的改变原型对象的属性就可以动态的影响所有基于此原型的对象;

另外,基于一个原型被创建出来的对象可以重载任何属于这个原型对象的方法、属性而不影响原型对象;

同时,基于原型被创建出来的对象还可以作为原型来创建其他对象。


13、函数环境

function foo()print(g or "No g defined!")endfoo()setfenv(foo, { g = 100, print = print })  --设置foo的环境为表{ g=100, ...}foo()print(g or "No g defined!")


输出结果:
No g defined!
100
No g defined!


函数环境定义
函数环境就是函数在执行时所见的全局变量的集合,以一个表来承载。

说明 
每个函数都可以有自己的环境,可以通过setfenv来显示的指定一个函数的环境。如果不显示的指定,函数的环境缺省为定义该函数的函数的环境。
在前面的代码中,函数foo的缺省环境里没有定义变量g,因此第一次执行foo, g为nil,表达式g or "No g defined!"的值就是"No g defined!"。
随后,foo被指定了一个环境 { g = 100, print = print }。这个环境定义了(全局)变量g,以及打印函数print,因此第二次执行foo,g的值就是
100。但是在定义函数foo的函数的以外的环境下,g仍然是一个未定义的变量。


应用
函数环境的作用很多,利用它可以实现函数执行的“安全沙箱”;另外Lua的包的实现也依赖它。


14、包

 包定义

包是一种组织代码的方式。


实现方式
一般在一个Lua文件内以module函数开始定义一个包。module同时定义了一个新的包的函数环境,以使在此包中定义的全局变量都在这个环境中,而非使用包的函数的环境中。理解这一点非常关键。


 “module(..., package.seeall)”的意思是定义一个包,包的名字与定义包的文件的名字相同(除去文件名后缀,在下面的代码中,就是“mypack”),并且在包的函数环境里可以访问使用包的函数环境(比如,包的实现使用了print,这个变量没有在包里定义,而是定义在使用包的外部环境中)。


使用方式
一般用require函数来导入一个包,要导入的包必须被置于包路径(package path)上。包路径可以通过package.path或者环境变量来设定。

一般来说,当前工作路径总是在包路径中。

全局变量_G

在Lua脚本层,Lua将所有的全局变量保存在一个常规的table中,这个table被称为全局环境,并且将这个table保存在一个全局变量_G中,也就是说在脚本中可以用_G获取这个全局table。变量_G是在C中注册的(源码linit.c, lbaselib.c中)。


其他
请参考Lua手册进一步了解包的详细说明。


--testP.lua:pack = require"mypack" --导入包print(ver or "No ver defined!")print(pack.ver)print(aFunInMyPack or "No aFunInMyPack defined!")pack.aFunInMyPack()print(aFuncFromMyPack or "No aFuncFromMyPack defined!")aFuncFromMyPack()--mypack.lua:module(..., package.seeall) --定义包ver = "0.1 alpha"function aFunInMyPack()print("Hello!")end_G.aFuncFromMyPack = aFunInMyPack


执行testP.lua的输出结果:

No ver defined!
0.1 alpha
No aFunInMyPack defined!
Hello!
function: 003CBFC0
Hello!



15、迭代

 迭代定义

迭代是for语句的一种特殊形式,可以通过for语句驱动迭代函数对一个给定集合进行遍历。

正式、完备的语法说明较复杂,请参考Lua手册。


实现
如前面代码所示:enum函数返回一个匿名的迭代函数,for语句每次调用该迭代函数都得到一个值(通过element变量引用),若该值为nil,则for循环结束。


function enum(array)local index = 1--index在这里成为了upvaluereturn function()local ret = array[index]index = index + 1return retendendfunction foreach(array, action)for element in enum(array) doaction(element)endendforeach({1, 2, 3}, print)


输出结果:
1
2
3



16、协作线程

(1)创建协作线程

通过coroutine.create可以创建一个协作线程,该函数接收一个函数类型的参数作为线程的执行体,返回一个线程对象。


(2)启动线程

通过coroutine.resume可以启动一个线程或者继续一个挂起的线程。该函数接收一个线程对象以及其他需要传递给该线程的参数。线程可以通过线程函
数的参数或者coroutine.yield调用的返回值来获取这些参数。当线程初次执行时,resume传递的参数通过线程函数的参数传递给线程,线程从线程函
数开始执行;当线程由挂起转为执行时,resume传递的参数以yield调用返回值的形式传递给线程,线程从yield调用后继续执行。


(3)线程放弃调度

线程调用coroutine.yield暂停自己的执行,并把执行权返回给启动/继续它的线程;线程还可利用yield返回一些值给后者,这些值以resume调用的返回值的形式返回。
 

(4)协作线程应用例子

例子1

function producer()return coroutine.create(function (salt)local t = { 1, 2, 3 }for i = 1, #t dosalt = coroutine.yield(t[i] + salt)endend)end


function consumer(prod)local salt = 10while true dolocal running, product = coroutine.resume(prod, salt)salt = salt * saltif running thenprint(product or "END!")elsebreakendendendconsumer(producer())

 

输出结果:
11
102
10003
END!




例子2

function instream()return coroutine.wrap(function()while true dolocal line = io.read("*l")if line thencoroutine.yield(line)elsebreakendendend)endfunction filter(ins)return coroutine.wrap(function()while true dolocal line = ins()if line thenline = "** " .. line .. " **"coroutine.yield(line)elsebreakendendend)endfunction outstream(ins)while true dolocal line = ins()if line thenprint(line)elsebreakendendendoutstream(filter(instream()))


输入/输出结果:
abc
** abc **
123
** 123 **
^Z


例子3

 Unix管道与Stream IO

利用协作线程可以方便地设计出类似Unix管道或者Stream IO的结构。(线程内通信)

function enum(array)return coroutine.wrap(function()local len = #arrayfor i = 1, len docoroutine.yield(array[i])endend)endfunction foreach(array, action)for element in enum(array) doaction(element)endendforeach({1, 2, 3}, print)



输出结果:
1
2
3


协作线程可以作为for循环迭代器的另一种实现方式。

虽然对于简单的数组遍历来说,没有必要这么做,但是考虑一下,如果需要遍历的数据集合是一个复杂数据结构,比如一棵树。

然而,在实际效率上,因为协程不是多线程,实际上效率没有得到提高。实用意义需要再思考。


17、常用函数


assert(v [, message]) -- 用于调试,断言v一定为true,否则说明有逻辑错误,中断运行并显示message的内容。
tostring(e) -- 将e转换成字符串
tonumber(e [, base])-- 将e转换成数值,如果指定base,则按指定的进制转换
next(table [, index]) -- 取数据表中index的下一个索引键;如果省略index则返回第一个;如果index为最后一个则返回nil
error(message [, level]) -- 终止执行并给出错误信息

18、数学库函数

math.abs     math.acos    math.asin    math.atan    math.atan2
math.ceil    math.cos     math.deg     math.exp     math.floor
math.log     math.log10   math.max     math.min     math.mod
math.pow     math.rad     math.sin     math.sqrt    math.tan
math.frexp   math.ldexp   math.random  math.randomseed


0 0