Lua学习笔记——核心(贰)
九、多脚本执行
1、全局变量和本地变量
全局变量可在任意地方使用,而本地变量只在当前作用域下使用,默认为全局变量,加上 local 关键字变为局部变量:
-- 全局变量,默认使用的都是全局变量
a = 1
for i=1,1 do
b = "hello"
end
print(b) -- hello
-- 本地(局部)变量,需要在变量前加local
for i=1,1 do
local c = "hello"
print(c) -- hello
end
print(c) -- nil
2、多脚本执行及脚本卸载
当前文件夹下新建一个 lua 脚本,名为 test.lua,内容如下:
一个为全局变量,一个在当前脚本声明为本地变量
-- test.lua
print("test脚本执行")
A = 123
local localA = 456
多脚本执行(同级文件夹下):
require(脚本名)
脚本卸载(判断脚本是否被执行):
package.loaded[脚本名] = nil
使用 require 函数执行该脚本,可以发现 localA变量失去了它的作用域,并且 require 执行过后便不能再执行了。需要在 package.loaded 表中将值置空,即脚本卸载后才能再次执行
-- 1、多脚本执行
require("test") -- 输出:test脚本执行
print(A) -- 123
print(localA) -- nil,此变量不在它的文件内使用,即出了作用域,失效
require("test") -- 第二次不执行
-- 2、脚本卸载
print(package.loaded["test"]) -- true
package.loaded["test"] = nil -- 将这个值置nil,即为卸载
require("test") -- 可以再次执行脚本
下面还有一种用法可以讲下:
修改 test.lua 脚本,将本地变量返回出去
print("test脚本执行")
A = 123
local localA = 456
return localA
在 require 函数调用时接收返回值,这样就可以使用别的脚本的本地变量了。
local myLocalA = require("test") -- 可接收返回值
print(myLocalA) -- 456
这样做可以实现模块化,避免全局变量滥用,比如每个脚本代表一个模块,脚本里使用 local 关键字创建一个本地变量(比如用表模拟的类)并返回出去,别的脚本调用该脚本时可以接收该本地变量,进而调用里面的方法和属性
3、大G表
_G 表是一个总表,它将我们声明的所有全局变量都存储在其中,这个比较重要,后面面向对象将会用到该知识点。
-- _G表是一个总表(本质是table),存储所有声明的全局变量
for k,v in pairs(_G) do
print(k,v) -- 会输出所有全局变量及对应值
end
十、特殊用法
1、多变量赋值和多返回值:
这在函数部分其实已经讲过了:
赋值时,等号后面的值不够,会自动补 nil
赋值时,前面的变量不够,则多余的值会自动忽略
-- 1、多变量赋值
a,b,c,d = 1,"123",true -- 变量d被赋值为nil
-- 2、多返回值
function Test()
return 1,2,3,4
end
a,b,c = Test() -- 返回值4被忽略
2、模拟三目运算符:
Lua 不支持三目运算符,但是 lua 很灵活!通过 and 和 or 可以模拟三目运算符(前提是值都不为 nil)。
用法:
判断条件 and 条件为真的逻辑 or 条件为假的逻辑
首先 and 和 or 其实可以连接任意类型,不只是 boolean,而只有 nil 和 false 会被认定为假,其他类型的值都会被认定为真(包括数字 0 等等)
再来看短路原则的例子:
print(1 and 2) -- 打印2,判断到1时并不能完全推断结果,所以必须判断到2才行
print(nil and 1) -- 打印nil,根据短路原则,在判断nil时就已经知道结果了,因此后续就没有判断了
通过 and 和 or 的连接特性,加上逻辑运算的短路原则,让我们模拟一下三目运算符:
-- 模拟三目运算符
x = 1
y = 2
local res = (x>y) and x or y
print(res) -- 2
--[[
解释:前提是x和y本身都为真(不为nil),分两种情况
1、若(x>y)为true
此时and会继续判断,若x为真,or前面的式子都为真,根据短路原则,or后的式子无需判断,最后结果就是x
2、若(x>y)为false
根据短路原则,and后的x无需判断,即((x>y) and x)为false,等价于false or y,所以会继续判断or后的y,最后结果就是y
]]
十一、协同程序
协程的创建和执行:
通过一个函数,可以创建协程,主要有两种方式:
-- 1、返回线程对象(常用)
创建:co = coroutine.create(Func)
执行:coroutine.resume(co)
-- 2、返回函数对象
创建:co = coroutine.wrap(Func)
执行:co()
下面来举些例子吧!
-- 1、协程的创建
func = function()
print(123)
end
-- 方式1:coroutine.create创建(常用)
co = coroutine.create(func)
print(type(co)) -- thread,这样创建是一个线程类型
-- 方式2:coroutine.wrap创建
co2 = coroutine.wrap(func)
print(type(co2)) -- function,这样创建是一个函数类型
-- 2、协程的运行
-- 方式1:coroutine.resume执行,对应create的协程
coroutine.resume(co) -- 123
-- 方式2:函数执行,对应wrap的协程
co2() -- 123
协程的挂起:
挂起线程:
coroutine.yield()
括号可填想要返回的参数,但默认隐含第一个返回参数是 boolean 值,代表是协程状态(协程是否启动成功),第二个及以后才是想要返回的参数
-- 协程的挂起
func2 = function()
local i = 1
while true do
print(i)
i = i + 1
coroutine.yield(i) -- 协程挂起函数,可带返回值
end
end
-- 方式一:
co3 = coroutine.create(func2)
coroutine.resume(co3) -- 只打印了一次,打印1
coroutine.resume(co3) -- 每启动一次,执行一次,打印2
status,temp = coroutine.resume(co3) -- 打印3
print(status,temp) -- 打印true 4,返回值第一个代表协程执行状态
-- 方式二:
co4 = coroutine.wrap(func2)
co4() -- 1
co4() -- 2
temp = co4() -- 3
print(temp) -- 打印4,这种方式也可以接收返回值,只不过没有默认的第一个参数了
协程的状态:
三种状态:dead 结束、 suspended 暂停、 running 进行中
获取协程的状态:coroutine.status(协程对象)
获取正在运行的协程:coroutine.running()
-- 协程的状态
print(coroutine.status(co3)) -- suspended
print(coroutine.status(co)) -- dead
-- 获取正在运行的协程
print(coroutine.running()) -- nil,当前没有协程在执行
十二、元表
(一)概念
1、元表的概念:
元表是一个表,它通过一组键和相关的元方法来帮助修改与其关联的表的行为。这些元方法是强大的 Lua 功能,比如实现类似 c# 的 tostring,重载运算符等等,当子表进行特定操作时,便会调用元表对应的元方法。
元表非常重要哦,因为元表有点父子关系那种继承的感觉,理解它可以更好的理解后面面向对象的内容。
元表概念
1、任何表都可以作为另一个表的元表
2、任何表都可以有自己的元表
3、当对子表中进行一些特定操作时,会执行元表中的内容
2、设置和获取元表:
设置元表:setmetatable (子表,元表)
返回值是设置完后的子表
获取子表的元表:getmetatable(子表)
-- 1、设置元表
meta = {}
myTable = {}
-- setmetatable函数
setmetatable(myTable,meta) -- 将meta表设置为myTable的元表
-- 2、获取元表(两表地址一样,说明获取到了)
print(meta) -- table: 00D99F00
print(getmetatable(myTable)) -- table: 00D99F00
(二)元方法
元方法是元表中的特殊键的值所指函数,定义了表在特定操作时的行为。
前面说过,元表是帮助修改与其关联的子表的行为,当子表出现某些行为后(事件),就会调用对应的方法,这个就是元方法。元表中这些特殊的键值对,键称为事件,值称为元方法。
元方法非常强大,我们可以通过自定义一些特定操作来实现自定义的行为,下面就来讲解下几个元方法。
1、__tostring:当子表要被当作字符串使用时,默认调用元表中的__tostring 方法
meta2 = {
__tostring = function()
return "table2"
end
}
myTable2 = {}
setmetatable(myTable2,meta2)
print(myTable2) -- 打印table2,不声明__tostring时只会打印地址
可以传参数进去,但默认会将调用者传进去,类似冒号调用函数:
-- __tostring
meta2 = {
__tostring = function(table)
return table.name
end
}
myTable2 = {
name = "Tom"
}
setmetatable(myTable2,meta2)
print(myTable2) -- 打印Tom
2、__call:当子表被当作一个函数使用时,默认调用元表中的__call 方法
meta3 = {
__call = function()
print("call")
end
}
myTable3 = {}
setmetatable(myTable3,meta3)
myTable3() -- call
同理,可以传参进去,但默认第一个参数传的是自己:
-- __call
meta3 = {
__call = function(mytable,age)
print("name:"..mytable.name.." age:"..age)
end
}
myTable3 = {
name = "Tom"
}
setmetatable(myTable3,meta3)
myTable3(18) -- name:Tom age:18
3、运算符重载
里面包含了很多元方法,但用法都类似,举 1 个例子就懂啦,我们以重载 + 号为例

-- 运算符重载
meta4 = {
-- 相当于运算符重载,当子表使用+运算符时调用
__add = function(t1,t2)
return t1.age + t2.age
end
}
myTable4 = {age = 18}
setmetatable(myTable4,meta4)
myTable5 = {age = 22}
print(myTable4 + myTable5) -- 40
注意几个点:
① 注意元方法中没有大于和大于等于,因为 a>b 可以转换成 b<a,即这种情况下会调用__lt 方法,相当于传参换了个位置
② 当使用 == 比较两个表时,这两个表的元表必须是一样的,否则不会调用元表的__eq 方法
4、__index(重点):当子表中找不到某个属性时,会到元表的__index 指定的表去找索引
meta6 = {
__index = {age = 6} -- 指向一个表
}
myTable6 = {}
setmetatable(myTable6,meta6)
print(myTable6.age) -- 打印6,这里myTable6找不到age属性,跑到元表__index指定的表去找了
注意几个点:
① 在内部指向自己的表时,会报 nil,因为此时 meta6 表还没有完全创建完成,这样只能通过外部进行设置,所以__index 可以统一都写在元表外面,避免报 nil
meta6 = {
age = 8,
-- __index = meta6 -- 这里声明自己会变成nil
}
meta6.__index = meta6 -- 可以在元表外部声明
myTable6 = {}
setmetatable(myTable6,meta6)
print(myTable6.age) -- 打印8,在元表中找到了age属性
② __index 可以嵌套,如果元表也设置了一个元表(暂时称为父元表吧),并且父元表也写了__index 方法,那么当子表找不到键时,回到元表中找,元表也找不到时,回到父元表(元表的元表)去找,可以一直嵌套下去
5、__newindex:当对表中一个不存在的索引赋值,那么这个值就会赋值到元表__newindex 所指的表中
meta7 = {}
meta7.__newindex = meta7 -- 将表设置为自己
myTable7 = {}
setmetatable(myTable7,meta7)
-- 下面这个例子可以看出,子表赋的值跑到元表__newindex所指的表中去了
myTable7.age = 18
print(myTable7.age) -- nil
print(meta7.age) -- 18
几个注意的点:
① __index 和__newindex 的区别就是前一个是访问时调用,后一个是修改时调用
② 同理,__newindex 也支持嵌套
6、rawget 和 rawset
rawget 针对__index,代表只获取该表的键值,忽略__index 设置:rawget (表,索引)
rawset 针对__newindex,代表只修改该表的键值,忽略__newindex 设置:rawset (表,索引,值)
-- rawget 只在自己身上找有没有这个变量,忽略__index
print(myTable6.age) -- 打印6,因为在元表的__index找到了
print(rawget(myTable6,"age")) -- 打印nil,因为该表中没有该索引
-- rawset 只在自己的表中修改变量,忽略__newindex
print(myTable7.age) -- 打印nil,目前该值在元表中
rawset(myTable7, "age", 20)
print(myTable7.age) -- 20
十三、面向对象
Lua 中的面向对象实现实现方式很灵活,这里就用我学习的方式来吧,我会创建一个万物之父 Object 类,实现通用的 new 方法进行实例化和 subClass 方法进行继承。
(一)封装
想要实现面向对象,首先要实现的就是类的实例化,所以我们可以为类添加一个 new 方法。
实现 new 方法来创建类对象,本质上就是创建了一个新的表变量,并将类设置为该新对象元表,通过元表和冒号的机制来实现类的封装。
-- 万物之父Object
Object = {}
Object.id = 1
function Object:Test()
print(self.id)
end
-- 创建对象:new方法
function Object:new()
local obj = {}
self.__index = self -- 设置__index,指向元表自己
setmetatable(obj,self) -- 设置新对象的元表
return obj
end
-- 测试:
local myObj = Object:new()
print(myObj.id) -- 1
myObj:Test() -- 1
myObj.id = 2
print(Object.id) -- 输出1,并没有修改元表的属性
print(myObj.id) -- 输出2,因为当前表中已经有id了,不用从元表中找了
(二)继承
通过自己实现 subClass 方法,结合_G 表创建一个全局变量,该变量代表继承的子类,这样外部就可以使用这个子类进行实例化了。
继承方法其实和 new 方法类似,唯一区别就是继承返回的是全局变量(用到_G 表),实例化返回的是局部变量(新建变量),因此继承另一种方式就是不写 subClass 方法,而是直接调用 Object 的 new 方法创建一个类,用全局变量接收。
当然,我就按实现 subClass 方法的方式了,然后注意到还有一点不同的是,我创建了一个 base 属性,指向父对象,这个是为多态准备的。
-- 继承方法:subClass方法
function Object:subClass(className)
_G[className] = {}
local obj = _G[className]
self.__index = self
setmetatable(obj,self)
obj.base = self -- 声明一个base属性,方便多态调用
end
--[[
这个subClass写法和创建对象的new方法很像,
其实也可以直接调用父类new方法来达到继承的目的,前面不带local即可,代表一个全局变量:
Person = Object:new()
]]
-- 测试:
Object:subClass("Person")
print(Person.id) -- 打印1,_G表中声明后,就可以直接使用了
p1 = Person:new()
print(p1.id) -- 1
(三)多态
多态指相同的方法,在不同类型的对象上具有不同的执行逻辑
通过上面两个自己写的方法,就可以实现多态了,下面这个例子中 Player 重写了父类 GameObject 的 Move 方法,重写的同时再次调用了父类自己的方法(用上了之前申明的 base 属性):
-- 声明一个GameObject类
Object:subClass("GameObject")
function GameObject:Move()
print("父类移动方法")
end
-- 声明GameObject的子类Player
GameObject:subClass("Player")
-- 重写父类方法
function Player:Move()
self.base:Move()
print("子类移动方法")
end
local p1 = Player:new()
p1:Move() -- 父类移动方法 子类移动方法
不过目前这里仍然有一个大坑!
在父类 Gameobject 声明一个变量 pos,并在 Move 方法中,每次调用会将 pos+1,子类重写 Move 方法后,直接通过 base 调用父类的 Move 方法即可。此时声明两个 Player 实例 p1 和 p2,分别调用 Move 方法,查看打印结果,此时会发现它们似乎共用了一个变量!
Object:subClass("GameObject")
GameObject.pos = 0
function GameObject:Move()
self.pos = self.pos + 1
print("当前位置:"..self.pos)
end
GameObject:subClass("Player")
-- 重写父类方法
function Player:Move()
self.base:Move()
end
local p1 = Player:new()
p1:Move() -- 当前位置:1
local p2 = Player:new()
p2:Move() -- 当前位置:2
经过仔细分析,会发现问题其实发生在 Player 重写的 Move 方法里:
self.base:Move()
还记的冒号调用函数的规则吗?通过这句调用父类方法时,Move 默认第一个参数传入的是 base,即 GameObject 本身,而不是 self(即调用者)。所以 GameObject 的 Move 方法被调用时,Move 方法里的 self 一直指的是 GameObject,而非 p1 和 p2,因此会觉得它们共用了一个变量。
正确的解决办法是不用冒号去调用父类的方法了,改用点号,传入的第一个参数设为 self 即可:
self.base.Move(self)
此时结果就正确了:
Object:subClass("GameObject")
GameObject.pos = 0
function GameObject:Move()
self.pos = self.pos + 1
print("当前位置:"..self.pos)
end
GameObject:subClass("Player")
-- 重写父类方法
function Player:Move()
self.base.Move(self)
end
local p1 = Player:new()
p1:Move() -- 当前位置:1
local p2 = Player:new()
p2:Move() -- 当前位置:1
十四、深拷贝
1、概述:
Lua 中对变量进行赋值时,会有两种情况:
(1)对值进行复制:
即变量存储的是对象的副本,会创建一个新对象,拷贝出来的对象和原来的互不影响,比如四个基本类型 nil、number、string、boolean
(2)传递值本身的引用:
即变量存储的是对象的引用,我们操作的是这些对象的引用,赋值对象和原来是一个对象,改一处另一处也会变化,比如后面四种类型:userdata、function、thread、table
对于引用传递的四种类型,userdata、function、thread 大部分情况下都无需考虑拷贝的问题,深拷贝意义不大,因此只需考虑table的深拷贝问题,其他对象直接原封不动返回即可。
2、深拷贝方法实现:
由于 Lua 中没有相关拷贝 api,因此需要自己实现一个 clone 方法,实现如下(附带讲解):
-- 深拷贝函数:
--[[
1、为啥需要两层函数:
外层函数用于提供一个表,用于记录已经拷贝过的表,
内层函数是真正执行逻辑的地方,用于递归的拷贝创建一个对象
2、内层函数分3个步骤:
(1)如果拷贝对象不是表,则直接返回值即可,这种值赋值时会自己复制
(2)如果拷贝对象是表,并且已经拷贝过,那直接把拷贝过的表返回即可
(3)如果拷贝对象是表,并且没有拷贝过,那就创建一个新的表返回即可
]]
function clone(object)
-- 记录已经拷贝过的表,防止循环引用,因为可能表自己会引用自己(表内部的表元素存的是表的引用)
local lookup_table = {}
-- 递归拷贝函数
local function _copy(object)
-- 该对象不是表,直接返回这个值
if type(object) ~= "table" then
return object
-- 已经深拷贝过该表了,直接返回即可
elseif lookup_table[object] then
return lookup_table[object]
end
-- 创建新表并记录
local new_table = {}
lookup_table[object] = new_table
-- 递归拷贝每个键值对,键和值都用_copy函数,因为键也可能是一个表
for key,value in pairs(object) do
new_table[_copy(key)] = _copy(value)
end
-- 为 拷贝的表 设置和 原来的表 相同的元表,并返回拷贝完的表
return setmetatable(new_table,getmetatable(object))
end
return _copy(object)
end
测试:
local tb1 = {x=1,y=2,z=3}
local tb2 = clone(tb1)
tb2.x = 5
print(tb1.x) -- 1
print(tb2.x) -- 5
-- 两者互不影响,拷贝成功!
3、对这部分代码的理解(为何使用_copy):
代码如下(深拷贝代码的28-30行):
-- 递归拷贝每个键值对,键和值都用_copy函数,因为键也可能是一个表
for key,value in pairs(object) do
new_table[_copy(key)] = _copy(value)
end
因为 key 或者 value 都有可能是一个 table 类型,因此都需要考虑深拷贝
举个例子就知道啦:
-- 这里value为表
local original = {
a = {1,2,3}
}
local copy = clone(original)
-- 打印原表:table: 00D9A3D8
for _,value in pairs(original) do
print(value)
end
-- 打印拷贝表:table: 00D97F60
for _,value in pairs(copy) do
print(value)
end
-- 可以发现两者的表地址是不一样的,说明递归创建了一个新的表
十五、其他
(一)一些常用的自带库
这部分知识算是了解即可吧,一般都是用到再查,简单列几个常用的:
1、时间
-- 时间
print(os.time())
print(os.time({year = 2014, month = 8, day = 14}))
local nowTime = os.date("*t")
print(nowTime.hour)
print(nowTime.year)
2、数学
-- 数学
print(math.abs(-11)) -- 绝对值:11
print(math.deg(math.pi)) -- 弧度转角度:180
print(math.cos(math.pi)) -- 余弦:-1
print(math.floor(2.6)) -- 向下取整:2
print(math.ceil(5.2)) -- 向上取整:6
print(math.max(1,2,4)) -- 最大值:4
print(math.min(2,1,4,3)) -- 最小值:3
print(math.modf(1.2)) -- 分割整数和小数部分:1 0.2
print(math.pow(2,5)) -- 幂运算:32
print(math.sqrt(4)) -- 开方:2
-- 随机数
math.randomseed(os.time()) -- 先设置随机数种子
print(math.random(100))
3、路径
-- 路径
print(package.path) -- lua脚本加载路径
-- 前面多脚本执行用到的package.loaded也是
(二)垃圾回收
这里只是简单讲解一下垃圾回收:
1、强引用与弱引用:
Lua 中正常设置的值,即有变量直接引用的值,则为强引用,垃圾回收机制不会对其进行回收。
Lua 的另一种引用类型则是弱引用,垃圾回收机制只要运行就会对弱引用进行回收
2、两个方法:
获取当前 lua 占用内存数:collectgarbage(“count”)
进行垃圾回收:collectgarbage(“collect”)
test = {id=1,name="123"}
-- 关键方法:collectgarbage
-- 获取当前lua占用内存数,单位K字节
print(collectgarbage("count")) -- 20.193359375
test = nil -- nil中断了test对table的强引用,只剩下弱引用对该table的引用,这样就会被回收
-- 进行垃圾回收(类似c#的GC)
collectgarbage("collect")
print(collectgarbage("count")) -- 19.2861328125