Lua知识点整理

基础

Lua 数据类型

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

boolean

boolean 类型只有两个可选值:true(真) 和 false(假),Lua 把 false 和 nil 看作是 false,其他的都为 true,数字 0 也是 true。

print(type(true))
print(type(false))
print(type(nil))

if false or nil then
    print("至少有一个是 true")
else
    print("false 和 nil 都为 false")
end

if 0 then
    print("数字 0 是 true")
else
    print("数字 0 为 false")
end

以上代码输出为:

boolean
boolean
nil
false 和 nil 都为 false
数字 0 是 true

type

type 函数测试给定变量或者值的类型

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

print(type(nil) == nil)         --> false
print(type(nil) == "nil")       --> true

注意type用于nil时,返回值是一个字符串'nil'

逻辑运算符

操作符描述
and逻辑与操作符。 若 A 为 false,则返回 A,否则返回 B。
or逻辑或操作符。 若 A 为 true,则返回 A,否则返回 B。

Lua的#运算符

local str="hello"
local t1 = { 1, 2, 3 }
local t2 = { [1] = 1, [2] = 2, [3] = 3, [4] = 5, [5] = 5 }
local t3 = { [0] = 0, [1] = 1, [2] = 2, [3] = 3, [5] = 5 }
local t4 = { [2] = 2, [3] = 3, [4] = 5, [5] = 5 }

print(#str)--5
print(#t1)--3
print(#t2)--5
print(#t3)--3
print(#t4)--0

运算符#为一元运算符,返回字符串或表的长度。使用于table时,有类似ipairs的性质,从下标为1开始后遍历,按顺序依次下标加1,若遇到下标的值为nil时便退出。

pairs和ipairs区别

local t1 = { 1, 2, 3, 4, 5 }
local t2 = { 1, 2, 3, nil, 5 }

local t3 = { [1] = 1, [2] = 2, [3] = 3, [5] = 5 }
local t4 = { [0] = 0, [1] = 1, [2] = 2, [3] = 3, [5] = 5 }

local t5 = { [0] = 0, [1] = 1, [2] = 2, [3] = 3, [4] = nil, [5] = 5 }

function printTableByPairs(t, prefix)
    local str = prefix .. ' pairs: ['
    for _, v in pairs(t) do
        str = str .. tostring(v) .. ','
    end
    str = str .. ']'
    print(str)
end

function printTableByIpairs(t, prefix)
    local str = prefix .. ' ipairs: ['
    for _, v in ipairs(t) do
        str = str .. tostring(v) .. ','
    end
    str = str .. ']'
    print(str)
end

printTableByPairs(t1, "t1")--t1 pairs: [1,2,3,4,5,]
printTableByIpairs(t1, "t1")--t1 ipairs: [1,2,3,4,5,]

printTableByPairs(t2, "t2")--t2 pairs: [1,2,3,5,]
printTableByIpairs(t2, "t2")--t2 ipairs: [1,2,3,]

printTableByPairs(t3, "t3")--t3 pairs: [1,2,5,3,]
printTableByIpairs(t3, "t3")--t3 ipairs: [1,2,3,]

printTableByPairs(t4, "t4")--t4 pairs: [0,2,3,1,5,]
printTableByIpairs(t4, "t4")--t4 ipairs: [1,2,3,]

printTableByPairs(t5, "t5")--t5 pairs: [0,2,3,1,5,]
printTableByIpairs(t5, "t5")--t5 ipairs: [1,2,3,]
  • pairs: 遍历表中全部key,value,顺序不确定
  • ipairs: 从下标为1开始后遍历,按顺序依次下标加1,若遇到下标的值为nil时便退出。

loadfile、dofile、require区别

  • loadfile:只编译,不运行
  • dofile:编译且执行
  • require:首次require某文件会加载且执行,然后会保存起来,之后再require不会再执行

pcall 和 xpcall、debug

Lua中处理错误,可以使用函数pcall(protected call)来包装需要执行的代码。pcall接收一个函数和要传递给后者的参数,并执行,执行结果:有错误、无错误;返回值true或者或false, errorinfo。pcall以一种"保护模式"来调用第一个参数,因此pcall可以捕获函数执行中的任何错误。

通常在错误发生时,希望落得更多的调试信息,而不只是发生错误的位置。但pcall返回时,它已经销毁了调用桟的部分内容。Lua提供了xpcall函数,xpcall接收第二个参数——一个错误处理函数,当错误发生时,Lua会在调用桟展开(unwind)前调用错误处理函数,于是就可以在这个函数中使用debug库来获取关于错误的额外信息了。

debug库提供了两个通用的错误处理函数:

  • debug.debug:提供一个Lua提示符,让用户来检查错误的原因
  • debug.traceback:根据调用桟来构建一个扩展的错误消息

参考:https://www.runoob.com/lua/lua-error-handling.html

Lua元表

在Lua table中我们可以访问对应的key来得到value值,但是却无法对两个table进行操作。
因此Lua提供了元表(Metatable),允许我们改变table的行为,每个行为关联了对应的元方法。

__index元方法

这是metatable最常用的键。当你通过键来访问table的时候,如果这个键没有值,那么Lua就会寻找该table的metatable(假定有metatable)中的index键。如果index包含一个表格,Lua会在表格中查找相应的键。

mytable = setmetatable({key1 = "value1"}, { __index = { key2 = "metatablevalue" } })
print(mytable.key1,mytable.key2)

以上代码输出结果:

value1    metatablevalue

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

  1. 在表中查找,如果找到,返回该元素,找不到则继续
  2. 判断该表是否有元表,如果没有元表,返回 nil,有元表则继续。
  3. 判断元表有没有 index 方法,如果 index 方法为 nil,则返回 nil;如果 index 方法是一个表,则重复 1、2、3;如果 index 方法是一个函数,则返回该函数的返回值。

__newindex 元方法

newindex元方法用来对表更新,index则用来对表访问。当你给表的一个缺少的索引赋值,解释器就会查找newindex元方法:如果存在则调用这个函数而不进行赋值操作。而如果对已存在的索引键,则会进行赋值,而不调用元方法newindex。

示例1:

mymetatable = {}
mytable = setmetatable({key1 = "value1"}, { __newindex = mymetatable })

print(mytable.key1)

mytable.newkey = "新值2"
print(mytable.newkey,mymetatable.newkey)

mytable.key1 = "新值1"
print(mytable.key1,mymetatable.key1)

以上代码输出结果:

value1
nil    新值2
新值1    nil

若索引已存在则新值覆盖旧值,若索引不存在,且newindex是一个表,新值会插入到newindex中。

示例2:

mytable = setmetatable({key1 = "value1"}, {
  __newindex = function(mytable, key, value)
                rawset(mytable, key, "\""..value.."\"")

  end
})

mytable.key1 = "new value"
mytable.key2 = 4

print(mytable.key1,mytable.key2)

以上代码输出结果:

new value    "4"

若索引不存在,且__newindex是一个函数,则会执行这个函数,函数接受3个参数:被赋值的表、key、value

表操作符

元方法对应的运算符
__add+
__sub-
__mul*
__div/
__mod%
__unm-
__concat..
__eq==
__lt<
__le<=

__call 元方法

__call 元方法在 Lua 调用一个值时调用。

local mytable = { 1, 2, 3 }
setmetatable(mytable, { __call = function(mytable, newtable)
    for k, v in pairs(mytable) do
        print(v)
    end

    for k, v in pairs(newtable) do
        print(v)
    end
end })

mytable({ 5, 6, 7 })

以上代码输出结果:

1
2
3
5
6
7

__tostring 元方法

__tostring 元方法用于修改表的输出行为。

mytable = setmetatable({ 10, 20, 30 }, {
  __tostring = function(mytable)
    sum = 0
    for k, v in pairs(mytable) do
                sum = sum + v
        end
    return "表所有元素的和为 " .. sum
  end
})
print(mytable)

以上代码输出结果:

表所有元素的和为 60

Lua实现面向对象

单继承

-- Meta class
Shape = {area = 0}
-- 基础类方法 new
function Shape:new (o,side)
  o = o or {}
  setmetatable(o, self)
  self.__index = self
  side = side or 0
  self.area = side*side;
  return o
end
-- 基础类方法 printArea
function Shape:printArea ()
  print("面积为 ",self.area)
end

-- 创建对象
myshape = Shape:new(nil,10)
myshape:printArea()

Square = Shape:new()
-- 派生类方法 new
function Square:new (o,side)
  o = o or Shape:new(o,side)
  setmetatable(o, self)
  self.__index = self
  return o
end

-- 派生类方法 printArea
function Square:printArea ()
  print("正方形面积为 ",self.area)
end

-- 创建对象
mysquare = Square:new(nil,10)
mysquare:printArea()

Rectangle = Shape:new()
-- 派生类方法 new
function Rectangle:new (o,length,breadth)
  o = o or Shape:new(o)
  setmetatable(o, self)
  self.__index = self
  self.area = length * breadth
  return o
end

-- 派生类方法 printArea
function Rectangle:printArea ()
  print("矩形面积为 ",self.area)
end

-- 创建对象
myrectangle = Rectangle:new(nil,10,20)
myrectangle:printArea()

以上代码输出结果:

面积为     100
正方形面积为     100
矩形面积为     200

多继承

在类的继承的基础上实现多继承,实际就是将metatable的index设置为一个可以在多个table中查找key的函数即可。如果有一个类A,想要集成类B和C,我们可以在类A中将B和C保存起来。然后给A设置一个metatable,metatable的index字段为一个函数,这个函数从类A保存的B和C中去查找需要的字段。

参考:https://www.jianshu.com/p/8b9bd43cfb06

进阶

require加载机制

对于自定义的模块,模块文件不是放在哪个文件目录都行,函数 require 有它自己的文件路径加载策略,它会尝试从 Lua 文件或 C 程序库中加载模块。

require 用于搜索 Lua 文件的路径是存放在全局变量 package.path 中,当 Lua 启动后,会以环境变量 LUA_PATH 的值来初始这个环境变量。如果没有找到该环境变量,则使用一个编译时定义的默认路径来初始化。

当然,如果没有 LUA_PATH 这个环境变量,也可以自定义设置,在当前用户根目录下打开 .profile 文件(没有则创建,打开 .bashrc 文件也可以),例如把 "~/lua/" 路径加入 LUA_PATH 环境变量里:

#LUA_PATH
export LUA_PATH="~/lua/?.lua;;"

文件路径以 ";" 号分隔,最后的 2 个 ";;" 表示新加的路径后面加上原来的默认路径。

接着,更新环境变量参数,使之立即生效。

source ~/.profile

这时假设 package.path 的值是:

/Users/dengjoe/lua/?.lua;./?.lua;/usr/local/share/lua/5.1/?.lua;/usr/local/share/lua/5.1/?/init.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/lib/lua/5.1/?/init.lua

那么调用 require("module") 时就会尝试打开以下文件目录去搜索目标。

/Users/dengjoe/lua/module.lua;
./module.lua
/usr/local/share/lua/5.1/module.lua
/usr/local/share/lua/5.1/module/init.lua
/usr/local/lib/lua/5.1/module.lua
/usr/local/lib/lua/5.1/module/init.lua

如果找到目标文件,则会调用 package.loadfile 来加载模块。否则,就会去找 C 程序库。

搜索的文件路径是从全局变量 package.cpath 获取,而这个变量则是通过环境变量 LUA_CPATH 来初始。

搜索的策略跟上面的一样,只不过现在换成搜索的是 so 或 dll 类型的文件。如果找得到,那么 require 就会通过 package.loadlib 来加载它。

参考:https://www.runoob.com/lua/lua-modules-packages.html

Lua浅拷贝和深拷贝

  • 浅拷贝:只拷贝值内容,对于引用内容,只是增加了一个指针指向已存在的内存地址,如果原地址内容发生改变,那么浅复制出来的对象也会相应的改变。
  • 深拷贝:增加了一个指针并且申请了一个新的内存,把所有值拷贝一份到新内存中,并使这个增加的指针指向这个新的内存

深拷贝实现:

function deepcopy(orig, copies)
    copies = copies or {}
    local orig_type = type(orig)
    local copy
    if orig_type == 'table' then
        if copies[orig] then
            copy = copies[orig]
        else
            copy = {}
            copies[orig] = copy
            for orig_key, orig_value in pairs(orig) do
                copy[deepcopy(orig_key, copies)] = deepcopy(orig_value, copies)
            end
            setmetatable(copy, deepcopy(getmetatable(orig), copies))
        end
    else -- number, string, boolean, etc
        copy = orig
    end
    return copy
end

参考:https://www.dazhuanlan.com/2019/12/06/5dea7262ec3e3/

userdata和light userdata

userdata

  • userdata直译就是自定义用户数,userdata提供了一块原始的内存区域,可以用来存储任何东西,并且,在Lua中 userdata没有任何预定义的操作。我们可以用userdata来表示c里面的一个struct。
  • 可以为每种userdata创建一个唯一的元表,来辨别不同类型的userdata,每当创建了一个userdata后,就用相应的元表来标记它,而每得到一个userdata后,就检查它是否拥有正确的元表,注意Lua代码中不能改变userdata的元表(当能增加已有元表的属性,比如对元表key为__index赋值)。通常是将这个元表存储在注册表中,也类型名作为key,元表为value。
  • userdata受Lua垃圾收集器来管理
  • Lua在释放完全userdata所关联的内存时,若发现userdata对应的元表还有__gc元方法,则会调用这个方法,并以userdata自身作为参数传入。利用该特性,可以再回收userdata的同时,释放与此userdata相关联的资源。

light userdata

light userdata是一种表示C指针的值(即void*),要将一个轻量级userdata放入栈中,只需要调用lua_pushlightuserdata即可。轻量级userdata只是一个指针而已。它没有元表,就像数字一样,轻量级userdata无须受垃圾收集器的管理。

参考:
https://www.cnblogs.com/herenzhiming/articles/6101767.html
https://blog.csdn.net/dugaoda/article/details/50259497

Lua字符串内存

String又细分为短字符串LUA_TSHRSTR和长字符串LUA_TLNGSTR两种,默认长度小于40的为LUA_TSHRSTR,使用全局stringtable进行管理。即所有短字符串都在stringtable中存放,相同字符串只会有一份实际数据拷贝,每份相同的TString对象只是存放一个hash值,用来索引stringtable。而长字符串则跟普通的GCObject没有差别,相同字符串在内存都是单独一份数据拷贝。在Lua5.1中,没有区分长短字符串,所有的字符串统一在stringtable中存在唯一拷贝。猜想这种改变一是因为长字符串出现相同的情况比较少,二是lua5.1的方式长字符串TString计算Hash是抽取部分字符进行运算,这样的计算方式可能被伪造导致不同字符串的hash值一样,但要是所有字符全用来计算hash又比较耗时。

参考:https://blog.csdn.net/ft1874478a/article/details/95307214

Lua闭包与UpValue

当 Lua 编译一个函数时,会生成一个原型。该原型包含有函数的虚拟机指令、常数值(数值、字符串等),以及一些调试信息。在运行期,任何时候只要 Lua 执行一个function...end表达式,它就会创建一个新的闭包。每个闭包都有一个对函数原型的引用、一个对环境的引用(环境其实是一个表,函数可在该表中索引全局变量),和一个数组,数组中每个元素都是一个对 upvalue 的引用,可通过该数组来存取外层的局部变量。

Lua用一种称为upvalue的结构来实现闭包。对任何外层局部变量的存取间接地通过upvalue来进行。upvalue最初指向栈中变量活跃的地方。当离开变量作用域时(超过变量生存期时),变量被复制到upvalue中。由于对变量的存取是通过upvalue里的指针间接进行的,因此复制动作对任何存取此变量的代码来说都是没有影响的。与内层函数不同的是,声明该局部变量的函数直接在堆栈中存取它的局部变量。

通过为每个变量至少创建一个upvalue并按所需情况进行重复利用,保证了未决状态(是否超过生存期)的局部变量(pendingvars)能够在闭包间正确地共享。为了保证这种唯一性,Lua为整个运行栈保存了一个链接着所有正打开着的upvalue(那些当前正指向栈内局部变量的upvalue)的链表。当Lua创建一个新的闭包时,它开始遍历所有的外层局部变量,对于其中的每一个,若在上述upvalue链表中找到它,就重用此upvalue,否则,Lua将创建一个新的upvalue并加入链表中。注意,一般情况下这种遍历过程在探查了少数几个节点后就结束了,因为对于每个被内层函数用到的外层局部变量来说,该链表至少包含一个与其对应的入口(upvalue)。一旦某个关闭的upvalue不再被任何闭包所引用,那么它的存储空间就立刻被回收。

一个函数有可能存取其更外层函数而非直接外层函数的局部变量。这种情况下,有可能当闭包创建时,此局部变量尚不存在。Lua使用flat闭包来处理这种情况。有了flat闭包,无论何时只要函数存取更外层的局部变量,该变量也会进入其直接外层函数的闭包中。这样,当一个函数被实例化时,所有进入其闭包的变量就在直接外层函数的栈或闭包中了。

参考:云风:The Implementation of Lua 5.0 中译

Lua中table的底层实现

Lua中的表是关联数组,即可以通过任何值(除了nil)来索引表项,表项可以存储任何类型的值。此外,表是动态的,当有数据加入其中(对不存在的表项赋值),或从中移除数据(将nil赋给表项)时,它们可以自动伸缩。

Lua_table

截至Lua4.0版,表都是严格地以散列表(哈希表)实现的:所有的键、值都明确地存在于表中。在Lua5.0中,对表被用作数组的情形使用了一种新的算法来进行优化:对于键是整数的表项,将不保存键,只将值存入一个真正的数组中。更准确地说,在Lua5.0中,表以一种混合型数据结构来实现,它包含一个散列表部分和一个数组部分。对于键、值对"x"→9.3,1→100,2→200,3→300,上图展示了一种可能的形式。注意右边的数组部分:它不保存整数键。只有在底层实现时才需要注意这点区别,其他情况下,即使是对虚拟机来说,访问表项也是由底层自动统一操作的,因而用户不必考虑这种区别。表会根据其自身的内容自动动态地使用这两个部分:数组部分试图保存所有那些键介于1和某个上限n之间的值。非整数键和超过数组范围n的整数键对应的值将被存入散列表部分。

当表需要增长时,Lua重新计算散列表部分和数组部分的大小。最初表的两个部分有可能都是空的。新的数组部分的大小是满足以下条件的最大的n值:1到n之间至少一半的空间会被利用(避免像稀疏数组一样浪费空间);并且n/2+1到n之间的空间至少有一个空间被利用(避免n/2个空间就能容纳所有数据时申请n个空间而造成浪费)。当新的大小计算出来后,Lua为数组部分重新申请空间,并将原来的数据存入新的空间。举例来说,假设a是一个空表,散列表部分和数组部分都是0大小。如果执行a[1]=v,那么表就需要增长以容纳新键。Lua会选择n=1作为新数组的大小(存储一个数据1→v)。散列表部分仍保持为空。

这种混合型结构有两个优点:

  1. 存取整数键的值很快,因为无需计算散列值。
  2. 相比于将其数据存入散列表部分,数组部分大概只占用一半的空间,因为在数组部分,键是隐含的,而在散列表部分则不是。

结论就是,如果表被当作数组用,只要其整数键是紧凑的(非稀疏的),那么它就具有数组的性能,而且无需承担散列表部分的时间和空间开销,因为这种情况下散列表部分根本就不存在。相反,如果表被当作关联数组用,而不是当数组用,那么数组部分就可能不存在。这种内存空间的节省很重要,因为在Lua程序中,常常创建许多小的表,例如,当用表来实现对象时。

Lua虚拟机

Lua解释器在执行Lua程序时,首先将源码进行语法分析并编译成虚拟机指令(opcode,操作码),然后执行这些指令。对每一个被编译的函数,Lua为其创建一个原型,原型中含有一个由该函数的虚拟机指令组成的数组、一个所有被该函数用到的常数值(TObjects,字符串或实数)的数组(译者注:这很重要,因为这避免了在指令码中直接包含常数值进而导致指令长度的膨胀。事实上,可以把这些常数看成具有只读属性的全局变量,对它们的处理和全局变量的处理是一致的)。

在十年的时间里(从1993年Lua发布开始),各种版本的Lua都使用基于堆栈的虚拟机。从2003年开始,随着Lua5.0的发布,Lua改用个基于寄存器的虚拟机。新的虚拟机也用堆栈分配活动记录,寄存器就在该活动记录中。当进入Lua程序的函数体时,函数从栈中预分配一个足以容纳该函数所有寄存器的活动记录。函数的所有局部变量都各占据一个寄存器。因此,存取局部变量是相当高效的。

使用寄存器式虚拟机消除了用堆栈式虚拟机时为了在栈中拷贝数据而必需要的大量出入栈(push/pop)指令。在Lua中,这些出入栈指令相当费时,因为它们需要拷贝带标志的值(taggedvalue,TObject)。因此寄存器结构既消除了昂贵的值拷贝操作,又减少了为每个函数生成的指令码数量。Davisal.[6]反对基于寄存器的虚拟机,并以了Java虚拟机的字节码作为反证。某些编译器作者也因为可以很容易在编译期间为堆栈式虚拟机生成代码而反对寄存器式虚拟机。

参考:
云风:The Implementation of Lua 5.0中译
https://zhuanlan.zhihu.com/p/35590292

Lua GC方式与原理

在Lua5.0以前,Lua使用的是一个非常简单的标记扫描算法。它从根集开始遍历对象,把能遍历到的对象标记为活对象;然后再遍历通过分配器分配出来的对象全集链表,把没有标记为活对象的其它对象都删除。

但是,Lua5.0支持userdata,它可以有gc方法,当userdata被回收时,会调用这个方法。所以,一遍标记是不够的,不能简单的把死掉的userdata简单剔除,那样就无法正确的调用gc了。所以标记流程需要分两个阶段做,第一阶段把包括userdata在内的死对象剔除出去,然后在死对象中找回有gc方法的,对它们再做一次标记复活相关的对象,这样才能保证userdata的gc可以正确运行。执行完gc的userdata最终会在下一轮gc中释放(如果没有在gc中复活)。userdata有一个单向标记,标记gc方法是否有运行过,这可以保证userdata的gc只会执行一次,即使在__gc中复活(重新被根集引用),也不会再次分离出来反复运行finalizer。也就是说,运行过finalizer的userdata就永久变成了一个没有finalizer的userdata了。

从Lua5.1开始,Lua实现了一个步进式垃圾收集器。这个新的垃圾收集器会在虚拟机的正常指令逻辑间交错分布运行,尽量把每步的执行时间减到合理的范围。

Lua5.1采用了一种三色标记的算法。每个对象都有三个状态:无法被访问到的对象是白色,可访问到,但没有递归扫描完全的对象是灰色,完全扫描过的对象是黑色。

我们可以假定在任何时间点,下列条件始终成立(Invariants):

  1. 所有被根集引用的对象要么是黑色,要么是灰色的。
  2. 黑色的对象不可能指向白色的。
  3. 那么,白色对象集就是日后会被回收的部分;黑色对象集就是需要保留的部分;灰色对象集是黑色集和白色集的边界。

随着收集器的运作,通过充分遍历灰色对象,就可以把它们转变为黑色对象,从而扩大黑色集。一旦所有灰色对象消失,收集过程也就完成了。

步进式GC比全量GC复杂,不能再只用一个量来控制GC的工作时间。对于全量GC,我们能调节的是GC的发生时机,对于lua5.0,就是2倍上次GC后的内存使用量;在5.1以后的版本中,这个2倍可以由LUA_GCSETPAUSE调节。另外增加了LUA_GCSETSTEPMUL来控制GC推进的速度,默认是2,也就是新增内存速度的两倍。lua用扫描内存的字节数和清理内存的字节数作为衡量工作进度的标准,有在使用的内存需要标记,没在使用的内存需要清理,GC一个循环大约就需要处理所有已分配的内存数量的总量的工作。这里2是个经验数字,通常可以保证内存清理流程可以跑的比新增要快。

在Lua5.2中,曾经引入分代gc,以一个试验特性提供,后来因为没有收到太多正面反馈,又在Lua5.3中移除。事实上Lua5.2提供的分代GC过于简单,的确有设计问题,未能很好的解决问题,在还没有发布的Lua5.4中,分代GC被重新设计实现。

参考:
https://blog.codingnow.com/2018/10/lua_gc.html

Lua热更新(热重载)

这里指游戏运行中Lua虚拟机启动后,对代码逻辑修改后直接生效,不需重启游戏,常用于开发过程中调试,不是指游戏启动时的版本更新

Lua的require(modelname)把一个lua文件加载存放到package.loaded[modelname]。当我们加载一个模块的时候,会先判断是否在package.loaded中已存在,若存在则返回该模块,不存在才会加载(loadfile),防止重复加载。

package.loaded是一个Table,其中包含了全局表_G、默认加载的模块(string,debug,package,io,os,table,math,coroutine)和用户加载的模块。

最简单粗暴的热更新就是将package.loaded[modelname]的值置为nil,强制重新加载:

function reload_module(module_name)
    package.loaded[modulename] = nil
    require(modulename)
end

这样做虽然能完成热更,但问题是已经引用了该模块的地方不会得到更新,因此我们需要将引用该模块的地方的值也做对应的更新。

function reload_module(module_name)
    local old_module = _G[module_name]
    package.loaded[module_name] = nil
    require (module_name)
    local new_module = _G[module_name]
    for k, v in pairs(new_module) do
        old_module[k] = v
    end
    package.loaded[module_name] = old_module
end

实际使用因为upvalue丢失的问题,非常容易产生bug,不宜用于线上环境。

参考:
https://gameinstitute.qq.com/community/detail/120538
https://blog.codingnow.com/2008/03/hot_update.html

Lua与C#交互

Xlua方案

C#调用Lua:

C#先调用Lua的dll文件(C语言写的库),dll文件执行Lua代码

Lua调用C#:

  1. Wrap方式:非反射机制,需要为源文件生成相应的wrap文件,当启动Lua虚拟机时,Wrap文件将会被自动注册到Lua虚拟机中,之后,Lua文件将能够识别和调用C#源文件。总结:Lua调用Wrap文件, Wrap文件调用C#源文件
  2. 反射机制

更具体参考:
https://zhuanlan.zhihu.com/p/137625945
https://zhuanlan.zhihu.com/p/146377267
https://zhuanlan.zhihu.com/p/38322991

Lua各个版本的区别

更具体参考:
https://zhuanlan.zhihu.com/p/96009862
https://blog.codingnow.com/2018/04/lua_54_nil_in_table.html