[Unity]-[HotUpdate]-[Lua] Lua的基本语法

Lua概念

lua是一种轻量、灵活且易于嵌入的脚本语言,适用于多种应用场景,特别是在游戏开发和嵌入式系统中。

lua是由C语言编写并以源代码形式开放。编译后仅仅100多kb。

特性

  • 轻量级;c语言源码开源,编译后仅100多kb。

  • 可扩展;Lua提供了非常易于使用的扩展接口和机制:由宿主语言(通常是C、C++、c#)提供这些功能,Lua可以使用它们,就像是本来就内置的功能一样。

  • 支持面向过程编程和函数编程。(procedure-oriented and functional programming)

  • 自动内存管理;只提供了一种通用类型的表(table),用它可以实现数组、哈希表、集合、对象等。

  • 语言内置模式匹配:闭包(closure);函数也可以看作一个值;提供多线程(协同线程,并非操作系统层面支持的线程)支持;

  • 通过闭包和table可以很方便的支持面向对象编程所需要的一些关键机制,比如数据抽象、虚函数、继承和重载等。

应用场景

  • 游戏开发
  • 独立应用脚本
  • Web 应用脚本
  • 扩展和数据库插件如:MySQL Proxy 和 MySQL WorkBench
  • 安全系统,如入侵检测系统

安装环境以及IDE

我在电脑上安装了lua53的编程环境,详情请见链接: https://blog.youkuaiyun.com/qq_44697754/article/details/133103543

IDE就是VSCode,注意环境变量以及Setting

cmd中执行lua需要使用 lua53 1.lua 这样的指令。

Lua语法

Lua基础语法讲解

注释

– 单行注释使用两个连字符-。

–[[
多行注释
多行注释
多行注释
–]]

标识符和关键字

标识符也就是变量名、方法名或者表字段以及其他用户定义实体的名称,注意命名还是以字母或下划线开头。而且区分大小写。

一般约定了以下划线开头链接一串全大写字母的名字(比如_VERSION)被保留用于lua的内部全局变量。

在默认情况下,变量总是认为是全局的。

全局变量不需要声明,给一个变量赋值后即创建了这个全局变量,访问一个没有初始化的全局变量也不会出错,只不过得到的结果是:nil。

示例:

> print(b)
nil
> b=10
> print(b)
10

关键字就是lua语法自己保留的全局名。是不能用于标识符的。

Lua语言类型解析

这里需要有两个基本概念,就是动态类型语言和静态类型语言、强类型语言和弱类型语言。

简单理解就是:

动态类型就是不需要指定变量类型,值可以直接赋值给变量,语言可以自己根据值判断变量类型,这就是动态,在运行时才确定变量的类型。静态类型在编译时就需要确定变量类型。

强类型就是不允许不同类型的值之间自由转换,具有一定的类型严格性。

lua就是动态类型的语言。同时在类型严格性方面表现出一定的弱类型特征,但它在某些方面表现出了一定的类型严格性。

Lua的8种数据类型

  • nil
    表示一个无效值,比如没有赋值的变量(条件判断中为false)。

  • boolean
    就是布尔值,true和false。

  • number
    表示双精度的实浮点数,也就是c#中的double.

  • string
    这个就是字符串string,用双引号或者单引号。也可以用[[…]]来表示多行的一块字符串。

  • function
    用c或者lua编写的函数,注意在lua中可以用c的api交互。

  • userdata
    表示任意存储在变量中的C数据结构。是一种用户自定义数据,用于表示一种由应用程序或 C/C++ 语言库所创建的类型,可以将任意 C/C++ 的任意数据类型的数据(通常是 struct 和 指针)存储到 Lua 变量中调用。

  • thread
    表示执行的独立线程,用于执行协同线程。在 Lua 里,最主要的线程是协同程序(coroutine)。它跟线
    程(thread)差不多,拥有自己独立的栈、局部变量和指令指针,可以跟其他协同程序共享全局变量和其他大部分东西。
    线程跟协程的区别:线程可以同时多个运行,而协程任意时刻只能运行一个,并且处于运行状态的协程只有被挂起(suspend)时才会暂停。

  • table
    lua中的表(table)其实是一个“关联数组(associative arrays)”,数组的索引可以是数字、字符串或者类型,在lua中,table的创建是通过“构造表达式”来完成的,最简单的构造就是{},即是一个空表。
    注意lua中table的默认索引值是从1开始的。

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

Lua变量

变量在使用前,需要在代码中进行声明,即创建该变量。

编译程序执行代码之前编译器需要知道如何给语句变量开辟存储区,用于存储变量的值。

Lua 变量有三种类型:全局变量、局部变量、表中的域。

Lua 中的变量全是全局变量,哪怕是语句块或是函数里,除非用 local 显式声明为局部变量。

局部变量的作用域为从声明位置开始到所在语句块结束。

变量的默认值均为 nil。

-- test.lua 文件脚本
a = 5               -- 全局变量
local b = 5         -- 局部变量

function joke()
    c = 5           -- 全局变量
    local d = 6     -- 局部变量
end

joke()
print(c,d)          --> 5 nil

do 
    local a = 6     -- 局部变量
    b = 6           -- 对局部变量重新赋值
    print(a,b);     --> 6 6
end

print(a,b)      --> 5 6
Lua变量赋值语句
  • 赋值是改变一个变量的值和改变表域的最基本的方法。
a = "hello" .. "world"
t.n = t.n + 1
  • Lua 可以对多个变量同时赋值,变量列表和值列表的各个元素用逗号分开,赋值语句右边的值会依次赋给左边的变量。
a, b = 10, 2*x       <-->       a=10; b=2*x
  • 遇到赋值语句lua会先计算右边所有的值然后再执行赋值操作,所以我们可以这样进行变量的交换值:
x, y = y, x                     -- swap 'x' for 'y'
a[i], a[j] = a[j], a[i]         -- swap 'a[i]' for 'a[j]'
  • 当变量个数和值的个数不一致时,Lua会一直以变量个数为基础采取以下策略:
    • 变量个数 > 值的个数 按变量个数补足nil
    • 变量个数 < 值的个数 多余的值会被忽略
a, b, c = 0
print(a,b,c)             --> 0   nil   nil
a, b, c = 0, 0, 0
print(a,b,c)             --> 0   0   0
  • 多值赋值经常用来交换变量,或将函数调用返回给变量:
a, b = f()  --这个f()函数返回值有两个。第一个赋值给a,第二个赋值给b。
  • 尽可能的使用局部变量,有两个好处:
    • 避免命名冲突,注意一般情况下,同名局部变量优先级比全局变量高(因为局部赋值迟)。
    • 访问局部变量的速度比全局变量快。
a = 2
local a = 1
print(a)
  • 索引,table的索引支持使用方括号[],也提供了 . 操作。
tab = {a,b,c,d,e,f,g}
tab["q"] = "h"
i = 3
print(tab[i],tab.q)    -- c  h

--[[
    这里的tab的全部内容作为列表可以判定为:
    1   2   3   4   5   6   7   q
    a   b   c   d   e   f   g   h
--]]

Lua循环语句及判定

  • while循环
    在条件为true的时候,让程序重复执行语句,执行语句前会检查条件是否为true。

  • for循环
    重复次数在for语句判断

  • repeat … until
    重复执行循环,直到指定条件为true为止。

循环嵌套

可以在循环内嵌套一个或多个循环语句(while do … end;for … do … end;repeat … until;)

  • break语句
    退出当前循环或语句,开始下面的代码

  • goto语句
    讲程序的控制点转义到一个标签处。

注意无限循环和死循环。
while(true)do … end

Lua流程控制

也就是条件判断,lua认为false和nil为假,true和非nil为真。要注意lua中0为真。

--[ 0 为 true ]
if(0)
then
    print("0 为 true")
end

条件判断语句

  • if(判断条件布尔值)then
    执行语句及逻辑
    end
    elseif(判断条件及布尔值)then
    执行语句及逻辑
    end
    else
    以上都不满足的执行语句
    end

Lua函数

lua中,函数是对语句和表达式进行抽象的主要方法,即可以用来做一些特殊的工作,也可以用来计算算法和逻辑。

lua提供了很多的内建函数,可以很方便的调用,如print()可以将传入的参数打入到控制台上。

lua函数主要有两个用途:

  • 完成指定的任务,这种情况下函数可以作为调用语句使用。比如委托、匿名等
  • 计算并返回值,这种情况下函数作为赋值语句的表达式使用。比如使用参数计算并将返回值作为结果。
函数定义格式

optional_function_scope function function_name( argument1, argument2, argument3…, argumentn)
function_body
return result_params_comma_separated
end

翻译

函数作用域 function 函数名(参数1、参数2、参数3…, 参数n)
函数体
return 结果值

解释
  • optional_function_scope: 该参数是可选的指定函数是全局函数还是局部函数,未设置该参数默认为全局函数,如果你需要设置函数为局部函数需要使用关键字 local。

  • function_name: 指定函数名称。

  • argument1, argument2, argument3…, argumentn: 函数参数,多个参数以逗号隔开,函数也可以不带参数。

  • function_body: 函数体,函数中需要执行的代码语句块。

  • result_params_comma_separated: 函数返回值,Lua语言函数可以返回多个值,每个值以逗号隔开。

示例

排序之冒泡排序

function BubbleSort(arr)
	local len = #arr;
	for i = 1,len do
		for j = 1,len-i do
			if (a[j] < a[j+1]) then
				a[j],a[j+1] = a[j+1],a[j];
			end
		end
	end
end

lua中可以将函数作为参数传递给函数,

myprint = function(param)
   print("这是打印函数 -   ##",param,"##")
end

function add(num1,num2,functionPrint)
   result = num1 + num2
   -- 调用传递的函数参数
   functionPrint(result)
end
myprint(10)
-- myprint 函数作为参数传递
add(2,5,myprint)

--这是打印函数 -   ##    10    ##
--这是打印函数 -   ##    7    ##

多返回值,函数可以返回多个值,比如string.find,其返回匹配串"开始和结束的下标"(如果不存在匹配串返回nil)。

> s, e = string.find("www.runoob.com", "runoob") 
> print(s, e)
5    10

lua中,在return后列出要返回的值的列表即可返回多值,如:

function maximum(a)
    local mi = 1
    local m = a[mi]
    for i,val in ipairs(a) do
        if val > m then
            mi = i
            m = val
        end
    end
    return m,mi
end

print(maximum({8,10,9,11,7}))
函数可变参数

Lua函数可以接受可变数目的参数,和C语言类似,在函数参数列表中使用三点...表示函数有可变的参数。

也可以将可变参数赋值给一个变量。

我们也可以通过 select(“#”,…) 来获取可变参数的数量;

有时候我们可能需要几个固定参数加上可变参数,固定参数必须放在变长参数之前;

通常在遍历变长参数的时候只需要使用 {…},然而变长参数可能会包含一些 nil,那么就可以用 select 函数来访问变长参数了:select(‘#’, …) 或者 select(n, …);

  • select(‘#’, …) 返回可变参数的长度。
  • select(n, …) 用于返回从起点 n 开始到结束位置的所有参数列表。
    调用 select 时,必须传入一个固定实参 selector(选择开关) 和一系列变长参数。如果 selector 为数字 n,那么 select 返回参数列表中从索引 n 开始到结束位置的所有参数列表,否则只能为字符串 #,这样 select 返回变长参数的总数。
do  
    function foo(...)  
        for i = 1, select('#', ...) do  -->获取参数总数
            local arg = select(i, ...); -->读取参数,arg 对应的是右边变量列表的第一个参数
            print("arg", arg);  
        end  
    end  
  
    foo(1, 2, 3, 4);  
end

Lua运算符

算数运算符、关系运算符、逻辑运算符、其他运算符

算数运算符
  • 加号 +
  • 减号 -
  • 乘法 *
  • 除法 /
  • 取余 %
  • 乘幂 ^
  • 负号 -
  • 整除 //在这里插入图片描述
a = 21
b = 10
c = a + b
print("Line 1 - c 的值为 ", c )
c = a - b
print("Line 2 - c 的值为 ", c )
c = a * b
print("Line 3 - c 的值为 ", c )
c = a / b
print("Line 4 - c 的值为 ", c )
c = a % b
print("Line 5 - c 的值为 ", c )
c = a^2
print("Line 6 - c 的值为 ", c )
c = -a
print("Line 7 - c 的值为 ", c )
c = a//b
print("Line 8 - c 的值为 ", c )
关系运算符
  • 等于 ==
  • 不等于 -=
  • 大于 >
  • 小于 <
  • 大于等于 >=
  • 小于等于 <=
逻辑运算符
  • 与 and
  • 或 or
  • 非 not
其他运算符
  • 连接字符串 …
  • 一元运算符 # (返回字符串和表的长度)
运算符优先级

从高到低的顺序:

    ^
    not    - (unary)
    *      /       %
    +      -
    ..
    <      >      <=     >=     ~=     ==
    and
    or

Lua字符串

字符串或串(String)是由数字、字母、下划线组成的一串字符。

在 Lua 中,字符串是一种基本的数据类型,用于存储文本数据。

Lua 中的字符串可以包含任意字符,包括字母、数字、符号、空格以及其他特殊字符。

Lua 语言中字符串可以使用以下三种方式来表示:

  • 单引号间的一串字符:

    local str1 = 'This is a string.'
    local str2 = "This is also a string."
    
  • 双引号间的一串字符:

    local str = "Hello, "
    str = str .. "World!"  -- 创建一个新的字符串并将其赋值给str
    print(str)  -- 输出 "Hello, World!"
    
  • [[]]间的字符串:

    local multilineString = [[
    This is a multiline string.
    It can contain multiple lines of text.
    No need for escape characters.]]
    
    print(multilineString)
    
字符串长度计算

可以使用 string.len函数或 utf8.len 函数,包含中文的一般用 utf8.len,string.len 函数用于计算只包含 ASCII 字符串的长度。

local myString = "Hello, 世界!"

-- 计算字符串的长度(字符个数)
local length1 = utf8.len(myString)
print(length1) -- 输出 10

-- string.len 函数会导致结果不准确
local length2 = string.len(myString)
print(length2) -- 输出 14
转义字符表在这里插入图片描述
字符串操作

链接:https://www.runoob.com/lua/lua-strings.html

Lua数组

数组就是相同数据类型的元素按一定顺序排列的集合,可以是一维数组也可以是多维数组。

在 Lua 中,数组不是一种特定的数据类型,而是一种用来存储一组值的数据结构。

实际上,Lua 中并没有专门的数组类型,而是使用一种被称为 “table” 的数据结构来实现数组的功能。

Lua 数组的索引键值可以使用整数表示,数组的大小不是固定的。

在 Lua 索引值默认是以 1 为起始,但你也可以指定 0 开始。也可以以负数为数组索引值。

一维数组

最简单的数组,结构是线性表。一维数组可以直接用for循环出数组的元素。

--创建一维数组
local firArray = {99,10,20,36,88}
local arrayNum = #firArray
print(firArray[2])  --   10
print(arrayNum)  --    5
for i = 1, #firArray do
    print(firArray[i])
end
多维数组

多维数组即数组中包含数组或一维数组的索引对应一个数组。

二维数组(其实就是表格)即:

123
999897
777675

这是一个三行三列的二维数组。

array = {}
for i = 1, 3 do
    array[i] = {}
    for j = 1, 3 do
        array[i][j] = i*j
    end
end
-- 访问数组
for i = 1, 3 do
    for j = 1, 3 do
        print(array[i][j])
    end
end

这个生成出来的table表示为:

table: 00000000012f6d20 {
[1] => table: 00000000012f6d20 {
[1] => 1
[2] => 2
[3] => 3
}
[2] => table: 00000000012f6d20 {
[1] => 2
[2] => 4
[3] => 6
}
[3] => table: 00000000012f6d20 {
[1] => 3
[2] => 6
[3] => 9
}
}

可以看出索引值有重复。

下面生成不同索引值的三行三列数组:

-- 初始化数组
array = {}
maxRows = 3
maxColumns = 3
for row=1,maxRows do
   for col=1,maxColumns do
      array[row*maxColumns +col] = row*col
   end
end

-- 访问数组
for row=1,maxRows do
   for col=1,maxColumns do
      print(array[row*maxColumns +col])
   end
end

生成的table的遍历表示为,可以看出索引值不同:

table: 0000000000ed8bc0 {
[1] => table: 0000000000ed8bc0 {
}
[2] => table: 0000000000ed8bc0 {
}
[3] => table: 0000000000ed8bc0 {
}
[4] => 1
[5] => 2
[6] => 3
[7] => 2
[8] => 4
[9] => 6
[10] => 3
[11] => 6
[12] => 9
}

Lua迭代器

迭代器(iterator)是一种对象,可以用来遍历标准模板库容器中的部分或全部元素,每个迭代器对象代表容器中的确定的地址。

在lua中迭代器是一种支持指针类型的结构,可以遍历集合的每一个元素。

泛型for迭代器

泛型for在自己内部保存迭代函数,实际上保存了三个值:迭代函数、状态常量、控制变量

泛型for迭代器提供了集合的key/value对,语法格式如下:

for k,v in pairs(t) do
    print(k,v)
end

上面代码中,k,v为变量列表;pairs(t)为表达式列表。

-- 示例
arrray = {"name", "allen"}

for key,val in ipairs(array) 
do
    print(key,val)
end

输出为:
1 name
2 allen

1为key
name为val

以上示例中我们使用了lua默认的迭代器函数ipairs。

看看泛型for的执行过程:

  • 首先,初始化,计算in后面表达式的值,表达式应该返回泛型for的需要的三个值:
    迭代函数、状态常量、控制变量;与多值赋值一样,如果表达式返回的结果个数不足三个会自动用 nil 补足,多出部分会被忽略。
  • 第二,将状态常量和控制变量作为参数调用迭代函数(注意,对于for结构来说,状态常量没有用处,仅仅在初始化的时候获取他的值并传递给迭代函数。)
  • 第三,将迭代函数的返回值赋给变量列表。
  • 第四,如果返回的第一个值为nil就循环结束,否则执行循环。
  • 第五,回到第二步再次调用迭代函数。

在lua中我们常常使用函数来描述迭代器,每次调用该函数就返回集合的下一个元素。lua的迭代器包含以下两个类型:

  • 无状态的迭代器
  • 多状态的迭代器
无状态的迭代器

无状态迭代器是指不保留任何状态的迭代器,因此在循环中我们可以利用无状态迭代器避免创建闭包花费额外的代价。

每一次迭代,迭代函数都是用两个变量(状态常量、控制变量)的值作为参数被调用,一个无状态的迭代器只利用这两个值就可以获取下一个元素。

无状态迭代器的典型例子就是ipairs()。他遍历了数组的每一个元素,元素的索引需要就是数值。

-- 实现迭代器简单案例,实现数字n的平方
function square(max,n)
    if n < max then
        n = n + 1
    return n , n^2
    end
end

for i,n in square,3,0
do
    print(i,n)
end

这个示例就可以很直接明了的看出for迭代器的算法是先将表达式的值算出来,然后再在for循环中遍历执行。

迭代的状态包括被遍历的表(遍历过程中不会改变的状态常量)、当前的索引下表(控制变量),ipairs和迭代函数都很简单,我们在lua中可以这样实现:

-- 迭代函数ipairs的简单实现
function iter(a,i)
    i = i + 1
    local v = a[i]
    if v then
        return i,v
    end
end

function ipairs(a)
    return iter,a,0
end

当 Lua 调用 ipairs(a) 开始循环时,他获取三个值:迭代函数 iter、状态常量 a、控制变量初始值 0;然后 Lua 调用 iter(a,0) 返回 1, a[1](除非 a[1]=nil);第二次迭代调用 iter(a,1) 返回 2, a[2]……直到第一个 nil 元素。

多状态的迭代器

很多情况下,迭代器需要保存多个状态信息而不是简单的状态常量和控制变量,最简单的方法是使用闭包,还有一种方法就是将所有的状态信息封装到table中,将table作为迭代器的状态常量,因为这种情况下可以将所有信息存放到table内,所以迭代函数通常不需要第二个参数。

示例:

array = {"Google", "Runoob"}

function elementIterator (collection)
   local index = 0
   local count = #collection
   -- 闭包函数
   return function ()
      index = index + 1
      if index <= count
      then
         --  返回迭代器的当前元素
         return collection[index]
      end
   end
end

for element in elementIterator(array)
do
   print(element)
end

-- Google
-- Runoob

Lua table

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

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

lua table是不固定大小的,可以根据需要扩容。

lua也是通过table来解决模块module、包package、对象object的,例如string.format表示使用format来索引 table string。

table 构造

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

-- 初始化表
mytable = {}

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

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

当我们为table a 并设置元素,然后将a赋值给b,则a与b都指向同一个内存。如果a设置为nil,则b同样能访问table的元素,如果没有指定的变量指向a,lua的GC会清理对应内存。

示例

-- 简单的 table
mytable = {}
print("mytable 的类型是 ",type(mytable))

mytable[1]= "Lua"
mytable["wow"] = "修改前"
print("mytable 索引为 1 的元素是 ", mytable[1])
print("mytable 索引为 wow 的元素是 ", mytable["wow"])

-- alternatetable和mytable的是指同一个 table
alternatetable = mytable

print("alternatetable 索引为 1 的元素是 ", alternatetable[1])
print("alternatetable 索引为 wow 的元素是 ", alternatetable["wow"])

alternatetable["wow"] = "修改后"

print("mytable 索引为 wow 的元素是 ", mytable["wow"])

-- 释放变量
alternatetable = nil
print("alternatetable 是 ", alternatetable)

-- mytable 仍然可以访问
print("mytable 索引为 wow 的元素是 ", mytable["wow"])

mytable = nil
print("mytable 是 ", mytable)
table操作

链接:
https://www.runoob.com/lua/lua-tables.html

  • 连接 : table.concat(tableName)
  • 插入 : table.insert(tableName,index,addString)
  • 移除 : table.remove(tableName)
  • 排序 : table.sort(tableName)
  • 极值 : table.maxn(tableName)
fruits = {"banana","orange","apple","grapes"}
print("排序前")
for k,v in ipairs(fruits) do
        print(k,v)
end

table.sort(fruits)
print("排序后")
for k,v in ipairs(fruits) do
        print(k,v)
end
--[[
排序前
1    banana
2    orange
3    apple
4    grapes
排序后
1    apple
2    banana
3    grapes
4    orange
]]--

--极值示例实现
function table_maxn(t)
  local mn=nil;
  for k, v in pairs(t) do
    if(mn==nil) then
      mn=v
    end
    if mn < v then
      mn = v
    end
  end
  return mn
end
tbl = {[1] = 2, [2] = 6, [3] = 34, [26] =5}
print("tbl 最大值:", table_maxn(tbl))
print("tbl 长度 ", #tbl)
--[[
tbl 最大值:    34
tbl 长度     3
--]]

注意极值的这个示例,当我们获取table的长度的时候无论是使用#,还是tabel.getn,都会在索引中断的地方停止计数,而导致无法获取正常的长度。

可以使用下面的方法代替:

function table_leng(t)
  local leng=0
  for k, v in pairs(t) do
    leng=leng+1
  end
  return leng;
end

Lua 模块module与包package

模块module

lua中模块类似于一个封装库,有标准的模块管理机制,可以把一些公用的代码放在一个文件里,以API的形式在其他地方调用,有利于代码的重用和降低代码耦合度。

实际上就是说代码插件或者代码工具库的意思。

lua的模块是变量、函数等已知元素组成的table,因此创建一个模块很简单,就是创建一个table,然后把需要导出的常量、函数放入其中,最后返回这个table就行。

以下为创建自定义模块module.lua的文件代码:

-- 文件名为 module.lua
-- 定义一个名为 module 的模块
module = {}
 
-- 定义一个常量
module.constant = "这是一个常量"
 
-- 定义一个函数
function module.func1()
    io.write("这是一个公有函数!\n")
end
 
local function func2()
    print("这是一个私有函数!")
end
 
function module.func3()
    func2()
end
 
return module

根据代码可以看出来,模块的结构就是一个table的结构,因此可以像操作调用table里的元素那样来操作调用模块里的常量和函数。

上面的func2有local声明为局部变量,即表示一个私有函数,因此从外部是不能访问的。

require函数

lua提供了一个名为require(需要)的函数来加载模块,要加载一个模块,只需要简单的调用就行。

require("ModuleName")
-- 或者
require "Module Name"

执行require后会返回一个有模块常量或函数组成的table,并且会定义一个包含table的全局变量。

-- test_module.lua 文件
-- module 模块为上文提到到 module.lua
require("module")
print(module.constant)
module.func3()

require引入模块需要注意的步骤与事项

require在加载和调用模块的时候,是通过一系列的搜索路径来查找并加载指定的模块文件。

  • 模块路径
    lua使用一个预定义的路径表package.path来查找模块。这个表是一个字符串列表,每个字符串代表一个搜索模式。这些模式定义了lua如何在文件系统中查找模块文件。

    如下:

        print(package.path)
    
        --[[
        D:\A_Soft_Learn\Lua\lua-5.3.6_Win64_bin\lua\?.lua;D:\A_Soft_Learn\Lua\lua-5.3.6_Win64_bin\lua\?\init.lua;D:\A_Soft_Learn\Lua\lua-5.3.6_Win64_bin\?.lua;D:\A_Soft_Learn\Lua\lua-5.3.6_Win64_bin\?\init.lua;D:\A_Soft_Learn\Lua\lua-5.3.6_Win64_bin\..\share\lua\5.3\?.lua;D:\A_Soft_Learn\Lua\lua-5.3.6_Win64_bin\..\share\lua\5.3\?\init.lua;.\?.lua;.\?\init.lua
        D:\A_Soft_Learn\Lua\lua-5.3.6_Win64_bin\lua\?.lua;D:\A_Soft_Learn\Lua\lua-5.3.6_Win64_bin\lua\?\init.lua;D:\A_Soft_Learn\Lua\lua-5.3.6_Win64_bin\?.lua;D:\A_Soft_Learn\Lua\lua-5.3.6_Win64_bin\?\init.lua;D:\A_Soft_Learn\Lua\lua-5.3.6_Win64_bin\..\share\lua\5.3\?.lua;D:\A_Soft_Learn\Lua\lua-5.3.6_Win64_bin\..\share\lua\5.3\?\init.lua;.\?.lua;.\?\init.lua
        --]]
    

    注意这些路径中每个条目都代表一个搜索路径,其中
    - ?.lua : 表示直接查找.lua文件。
    - ?\init.lua : 表示查找目录中的init.lua文件。

  • 模块名解析
    当你调用require["modulename"]时,lua会执行以下步骤:

    • 模块名标准化:
      modulename被转化为标准形式,例如moduleName被转化为moduleName.luamoduleName\init.lua
    • 查找模块:
      lua按照package.path中定义的顺序查找模块文件。
    • 加载模板:
      找到模块文件后,lua会尝试加载该文件,如果文件以及加载过(通过检查package.loaded表),lua将返回已经加载的模块。
  • 自定义路径
    你可以通过修改package.pathpackage.cpath(用于C模块)来添加自定义的搜索路径。例如:

    -- 添加一个新的搜索路径  
    package.path = package.path .. ";/my/custom/path/?.lua"
    
  • 模块缓存
    lua通过package.loaded表示缓存里已经加载的模块。如果模块已经加载,require会直接返回缓存中的模块。

  • 示例
    注意require("moduleName")指令里的moduleName一般是指文件名,文件名要与模块名相同,其次,在获取后,最好赋值给一个变量再调用。

    -- 模块文件
    -- mymodule.lua  
    return {  
        hello = function()  
            print("Hello, World!")  
        end  
    }
    
    -- 调用require(moduleName)
    -- main.lua  
    local mymodule = require("mymodule")  
    mymodule.hello()  -- 输出: Hello, World!
    
  • 注意

    • 模块文件应该返回一个table,这个表将作为模块的内容。
    • 如果模块有错,那么lua将抛出一个错误。
    • require是幂等的,即多次调用require("moduleName")只会加载一次模块文件,会保存在缓存中。
  • require内部细节

    • 如果找过目标文件,则会调用 package.loadfile 来加载模块。否则,就会去找 C 程序库。
    • 搜索的文件路径是从全局变量 package.cpath 获取,而这个变量则是通过环境变量 LUA_CPATH 来初始。
    • 搜索的策略跟上面的一样,只不过现在换成搜索的是 so 或 dll 类型的文件。如果找得到,那么 require 就会通过 package.loadlib 来加载它。
C包package

Lua和C是很容易结合的,使用 C 为 Lua 写包。

与lua中写包不同,C包在使用以前必须首先加载并加载,在大多数系统中最容易的实现方式是通过动态连接库机制。

lua在一个叫loadlib的函数内提供了所有的动态连接的功能。这个函数有两个参数:
- 库的绝对路径
- 初始化函数

local path = "/usr/local/lua/lib/libluasocket.so"
local f = loadlib(path, "luaopen_socket")

loadlib函数加载指定的库并且连接到lua,然而它并不打开库(也就是说没有调用初始化函数),反之他返回初始化函数作为lua的一个函数,这样我们就可以直接在lua中调用他。

如果加载动态库或者查找初始化函数时出错,loadlib将返回nil和错误信息。我们可以修改前面一段代码,使其检测错误然后调用初始化函数:

local path = "/usr/local/lua/lib/libluasocket.so"
-- 或者 path = "C:\\windows\\luasocket.dll",这是 Window 平台下
local f = assert(loadlib(path, "luaopen_socket"))
f()  -- 真正打开库

一般情况下我们期望二进制的发布库包含一个与前面代码段相似的 stub 文件,安装二进制库的时候可以随便放在某个目录,只需要修改 stub 文件对应二进制库的实际路径即可。

将 stub 文件所在的目录加入到 LUA_PATH,这样设定后就可以使用 require 函数加载 C 库了。

Lua元表(MetaTable)

在lua table中我们可以访问对应的Key来得到value值。但是却无法对两个table进行算法操作,比如相加减等等。

因此lua提供了元表MetaTable就是用来改变table的行为,每个行为关联了对应的元方法。

例如,使用元表我们可以定义lua如何计算两个table的相加操作a+b.

当lua试图对两个table进行相加时,先检查两者之一是否有元素,之后检查是否有一个叫__add的字段,若找到,则调用对应的值。__add等即时字段,其对应的值(往往是一个函数或是table)就是“元方法”(MetaMethods)。

说明白就是元表就是一个普通的lua table,但它包含了一些特殊的键值对,这些键值对就是对应的定义的元方法(MetaMethods),元方法允许开发者去拦截和自定义表的某些操作。

元方法(MetaMethods)说白了就是一些以双下划线(__)开头的特殊键(Key),它们对应了表在特定操作下的行为。

常见的元方法包括

详细链接教程: https://www.runoob.com/lua/lua-metatables.html

  • __index
    当访问表中不存在的键时,Lua会查找元表中的__index的元方法,它可以是一个表或者一个函数,用来提供缺失的值。
  • __newindex
    当给表中不存在的键赋值时,lua会查找元表中的__newindex元方法。它也可以是一个表或者一个函数,用来处理赋值操作。
  • __add__sub__mul
    这些元方法定义了表在进行算术运算时的行为。
  • __tostring
    改变print函数对表table的输出行为。
  • _call
    允许表被调用像函数一样。

有两个很重要的函数来处理元表:
- SetMetaTable(table.metatable)
对指定table设置元表(metatable),如果元表中存在_metatable键值,SetMetaTable就会失败。
- GetMetaTable(table)
返回对象的元表。

设置元表示例:

mytable = {}                          -- 普通表 
mymetatable = {}                      -- 元表
setmetatable(mytable,mymetatable)     -- 把 mymetatable 设为 mytable 的元表

--另一种写法
mytable = setmetatable({},{})
--以下为返回对象元表:
getmetatable(mytable)                 -- 这会返回 mymetatable

具体元表元方法代码示例:

--定义一个元表
-- 元表及元方法示例
print("元表及元方法示例")
local metaTab = {
    __add = function (a,b)
        local result = {}
        for i,v in ipairs(a) do
            table.insert(result,v)
        end
        for i,v in ipairs(b) do
            table.insert(result,v)
        end
        return result
    end,
    __sub = function (a,b)
        --这里仅作为示例,不实现实际减法操作
        return print("Subtracting")
    end
    --这里可以添加其他元方法。
}

--创建两个表,并将它们的元表设置为上面定义的元表
local a = setmetatable({1,2,3},metaTab)
local b = setmetatable({9,9,9},metaTab)

--测试加法和减法
print("#(a+b):"..#(a+b)) --输出:Adding 6 ,表示a+b之后的长度
--print((a+b))
print((a-b))   -- Subtracting  nil

以下是一个更复杂的示例,展示了如何使用元表来处理索引查找、赋值、长度获取和字符串转换等操作。

--这里演示一个复杂代码示例
-- 定义一个复杂的元表  
local complex_mt = {  
    __index = function(table, key)  
        print("Indexing with key: " .. tostring(key))  
        return rawget(table, key) or "default value for " .. tostring(key)  
    end,  
    __newindex = function(table, key, value)  
        print("Setting key: " .. tostring(key) .. " to value: " .. tostring(value))  
        rawset(table, key, value)  
    end,  
    __len = function(table)  
        print("Getting length")  
        return #table  
    end,  
    __tostring = function(table)  
        print("Converting to string")  
        return "Complex table"  
    end  
}  
  
-- 创建一个表,并将它的元表设置为上面定义的复杂元表  
local complex_table = setmetatable({}, complex_mt)  
  
-- 测试索引查找  
print(complex_table["nonexistent"])  -- 输出: Indexing with key: nonexistent default value for nonexistent  
  
-- 测试设置索引  
complex_table["new_key"] = "new_value"  -- 输出: Setting key: new_key to value: new_value  
  
-- 测试长度获取  
print(#complex_table)  -- 输出: Getting length 0(因为表中没有元素)  
  
-- 测试转换为字符串  
print(complex_table)  -- 输出: Converting to string Complex table

这里应该就看明白了,setmetatable就是返回一个表对象,它的参数是setmetatable(table,metaTable),意思就是第一个参数就是要创建或者要加元表/元方法的table,第二个参数被定义好的元表/元方法。

具体看这两个使用的区别

setmetatable(table,metatable)

setmetatable(parent_mt, {__tostring = function() return "Parent class" end}) 
instance = setmetatable({}, parent_mt) 

就是说后面的参数是可以作为元方法加到第一个参数的元表中,所以第二个例子中,第二个参数元表就是加入到第一个参数table中,然后把这个结果作为返回值赋值给一个table,再使用这个table去调用原方法。

使用元表实现对象继承示例

-- 定义一个父类元表  
local parent_mt = {  
    __index = function(table, key)  
        print("Accessing parent key: " .. tostring(key))  
        return rawget(parent_mt, key) or nil  -- 注意这里返回nil,因为父类元表本身不是表实例  
    end  
}  
  
-- 定义一个子类元表,它继承自父类元表  
local child_mt = {  
    __index = function(table, key)  
        print("Accessing child key: " .. tostring(key))  
        local value = rawget(child_mt, key)  
        if value ~= nil then  
            return value  
        else  
            return parent_mt[key]  -- 如果子类中没有找到,则查找父类  
        end  
    end  
}  
  
-- 为父类和子类元表设置字符串表示(仅用于演示)  
setmetatable(parent_mt, {__tostring = function() return "Parent class" end})  
setmetatable(child_mt, {__tostring = function() return "Child class" end})  
  
-- 创建父类实例和子类实例  
local parent_instance = setmetatable({}, parent_mt)  
parent_instance.value = "parent value"  
  
local child_instance = setmetatable({}, child_mt)  
child_instance.value = "child value"  
  
-- 测试访问父类和子类实例的属性和方法  
print(parent_instance.value)  -- 输出: Parent value  
print(child_instance.value)  -- 输出: Child value  
print(child_instance.nonexistent)  -- 输出: Accessing child key: nonexistent Accessing parent key: nonexistent(因为子类中没有找到,所以查找父类,但父类中也没有)

通过以上示例,可以看出元表在Lua中的强大功能。它允许开发者自定义表的行为,实现类似于面向对象编程中的继承、多态等特性。同时,元表也可以用于创建只读表、保护表中的某些键不被修改等高级功能。

Lua的协同程序(coroutine)

Lua 协同程序(coroutine)与线程比较类似:拥有独立的堆栈,独立的局部变量,独立的指令指针,同时又与其它协同程序共享全局变量和其它大部分东西。

协同程序可以理解为一种特殊的线程,可以暂停和恢复其执行,从而允许非抢占式的多任务处理。

协同是非常强大的功能,但是用起来也很复杂。

协程的基本语法

协同程序由 coroutine 模块提供支持。注意是内置模块。

使用协同程序,你可以在函数中使用 coroutine.create 创建一个新的协同程序对象,并使用 coroutine.resume 启动它的执行。协同程序可以通过调用 coroutine.yield 来主动暂停自己的执行,并将控制权交还给调用者。

协程使用的基本方法:

  • coroutine.create()
    创建coroutine,返回一个coroutine,参数是一个函数,当和resume配合使用时就唤醒了函数调用成功。
  • coroutine.resume()
    重启coroutine,和create配合使用启动协程。resume(恢复)。
  • coroutine.yield()
    挂起coroutine,将coroutine设置为挂起状态,这个和resume配合使用能有很多有用的效果。
  • coroutine.status()
    查看coroutine的状态,注意coroutine有三种状态,dead,suspended(暂停),running,具体什么时候有这样的状态请参考下面程序。
  • coroutine.wrap()
    创建coroutine,返回一个函数,一旦你调用这个函数,就进入coroutine,和create功能重复。
  • coroutine.running()
    返回正在跑的coroutine,一个coroutine就是一个线程,当使用running的时候,就是返回一个coroutine的线程号。
协程使用示例
function foo()
    print("协同程序 foo 开始执行")
    local value = coroutine.yield("暂停 foo 的执行")
    print("协同程序 foo 恢复执行,传入的值为: " .. tostring(value))
    print("协同程序 foo 结束执行")
end

-- 创建协同程序
local co = coroutine.create(foo)

-- 启动协同程序
local status, result = coroutine.resume(co)
print(result) -- 输出: 暂停 foo 的执行

-- 恢复协同程序的执行,并传入一个值
status, result = coroutine.resume(co, 42)
print(result) -- 输出: 协同程序 foo 恢复执行,传入的值为: 42

这里可以看出coroutine.create仅仅只是创建一个协程,创建协程的参数就是入口函数,然后用coroutine.resume来启动协程,进入了协程,因为在函数foo中有coroutine.yield所以就暂停了,然后在后面重新resume重启了,所以协程又执行了。resume的重启的第一个必须参数就是协程名字,第二参数就是传入写成的值。

需要注意的是,协同程序的状态可以通过coroutine.status函数获取,通过检查状态可以确定协同程序的执行情况,(如运行中running、已挂起suspended、已结束dead等)。

示例:

-- coroutine_test.lua 文件
-- 创建了一个新的协同程序对象 co,其中协同程序函数打印传入的参数 i
co = coroutine.create(
    function(i)
        print(i);
    end
)
-- 使用 coroutine.resume 启动协同程序 co 的执行,并传入参数 1。协同程序开始执行,打印输出为 1
coroutine.resume(co, 1)   -- 1

-- 通过 coroutine.status 检查协同程序 co 的状态,输出为 dead,表示协同程序已经执行完毕
print(coroutine.status(co))  -- dead
 
print("----------")

-- 使用 coroutine.wrap 创建了一个协同程序包装器,将协同程序函数转换为一个可直接调用的函数对象
co = coroutine.wrap(
    function(i)
        print(i);
    end
)
 
co(1)
 
print("----------")
-- 创建了另一个协同程序对象 co2,其中的协同程序函数通过循环打印数字 1 到 10,在循环到 3 的时候输出当前协同程序的状态和正在运行的线程
co2 = coroutine.create(
    function()
        for i=1,10 do
            print(i)
            if i == 3 then
                print(coroutine.status(co2))  --running
                print(coroutine.running()) --thread:XXXXXX
            end
            coroutine.yield()
        end
    end
)

-- 连续调用 coroutine.resume 启动协同程序 co2 的执行
coroutine.resume(co2) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3

-- 通过 coroutine.status 检查协同程序 co2 的状态,输出为 suspended,表示协同程序暂停执行
print(coroutine.status(co2))   -- suspended
print(coroutine.running())
 
print("----------")

coroutine.running就可以看出coroutine在底层实现就是一个线程。这个下面会细讲。

coroutine.create时就是在新线程中注册了一个事件。

当使用resume触发事件的时候,create的coroutine函数就被执行了,当遇到yield的时候就代表挂起当前线程,等待再次resume触发事件。

function foo (a)
    print("foo 函数输出", a)
    return coroutine.yield(2 * a) -- 返回  2*a 的值
end
 
co = coroutine.create(function (a , b)
    print("第一次协同程序执行输出", a, b) -- co-body 1 10
    local r = foo(a + 1)
     
    print("第二次协同程序执行输出", r)
    local r, s = coroutine.yield(a + b, a - b)  -- a,b的值为第一次调用协同程序时传入
     
    print("第三次协同程序执行输出", r, s)
    return b, "结束协同程序"                   -- b的值为第二次调用协同程序时传入
end)
        
print("main", coroutine.resume(co, 1, 10)) -- true, 4
print("--分割线----")
print("main", coroutine.resume(co, "r")) -- true 11 -9
print("---分割线---")
print("main", coroutine.resume(co, "x", "y")) -- true 10 end
print("---分割线---")
print("main", coroutine.resume(co, "x", "y")) -- cannot resume dead coroutine
print("---分割线---")

--[[
第一次协同程序执行输出    1    10
foo 函数输出    2
main    true    4
--分割线----
第二次协同程序执行输出    r
main    true    11    -9
---分割线---
第三次协同程序执行输出    x    y
main    true    10    结束协同程序
---分割线---
main    false    cannot resume dead coroutine
---分割线---
--]]

以上示例解释:

  • 调用resume,将协同程序唤醒,resume操作成功返回true,否则返回false;
  • 协同程序运行;
  • 运行到yield语句;
  • yield挂起协同程序,第一次resume返回;(注意:此处yield返回,参数是resume的参数)
  • 第二次resume,再次唤醒协同程序;(注意:此处resume的参数中,除了第一个参数,剩下的参数将作为yield的参数)
  • yield返回;
  • 协同程序继续运行;
  • 如果使用的协同程序继续运行完成后继续调用 resume方法则输出:cannot resume dead coroutine

从上面就可以看出,resumeyield的配合强大之处在于,resume处于主程中,它将外部状态(数据)传入到协同程序内部;而yield则将内部的状态(数据)返回到主程中。

生产者-消费者问题
local newProductor

function productor()
     local i = 0
     while true do
          i = i + 1
          send(i)     -- 将生产的物品发送给消费者
     end
end

function consumer()
     while true do
          local i = receive()     -- 从生产者那里得到物品
          print(i)
          if i >= 1000 then
            coroutine.close(newProductor)
          end
     end
end

function receive()
     local status, value = coroutine.resume(newProductor)
     return value
end

function send(x)
     coroutine.yield(x)     -- x表示需要发送的值,值返回以后,就挂起该协同程序
end

-- 启动程序
newProductor = coroutine.create(productor)
consumer()

--[[
输出:
1
2
3
4
5
6
7
……
1000
]]--
线程和协程的区别

线程与协同程序的主要区别在于:

一个具有多个线程的程序可以同时运行几个线程,而协同程序却需要彼此协作的运行。

在任一指定时刻只有一个协同程序在运行,并且这个正在运行的协同程序只有在明确的被要求挂起的时候才会被挂起。

协同程序有点类似同步的多线程,在等待同一个线程锁的几个线程有点类似协同。也就是线程安全的单一线程。

主要区别归纳如下:

  • 调度方式:线程通常由操作系统的调度器进行抢占式调度,操作系统会在不同线程之间切换执行权。而协同程序是非抢占式调度的,它们由程序员显式地控制执行权的转移。

  • 并发性:线程是并发执行的,多个线程可以同时运行在多个处理器核心上,或者通过时间片轮转在单个核心上切换执行。协同程序则是协作式的,只有一个协同程序处于运行状态,其他协同程序必须等待当前协同程序主动放弃执行权。

  • 内存占用:线程通常需要独立的堆栈和上下文环境,因此线程的创建和销毁会带来额外的开销。而协同程序可以共享相同的堆栈和上下文,因此创建和销毁协同程序的开销较小。

  • 数据共享:线程之间可以共享内存空间,但需要注意线程安全性和同步问题。协同程序通常通过参数传递和返回值来进行数据共享,不同协同程序之间的数据隔离性较好。

  • 调试和错误处理:线程通常在调试和错误处理方面更复杂,因为多个线程之间的交互和并发执行可能导致难以调试的问题。协同程序则在调试和错误处理方面相对简单,因为它们是由程序员显式地控制执行流程的。

总体而言,线程适用于需要并发执行的场景,例如在多核处理器上利用并行性加快任务的执行速度。而协同程序适用于需要协作和协调的场景,例如状态机、事件驱动编程或协作式任务处理。选择使用线程还是协同程序取决于具体的应用需求和编程模型。

Unity协程和lua协程的区别

先说最重要的一点,本质上,Unity协程是基于主线程上执行的,而Lua协程并不是主线程,是完全在lua虚拟机(lua VM)内部实现的轻量化线程,但是是在主线程的上下文中被管理和执行的。

Unity协程
  • 执行环境:Unity的协程是在主线程上执行的,它们不会创建新的线程。协程通过迭代器和yield关键字实现,允许开发者以同步的代码风格编写异步逻辑。
  • 生命周期:Unity协程的生命周期与MonoBehaviour的生命周期紧密相关。它们会在每一帧被Unity引擎调用和处理,直到协程执行完毕或遇到新的yield语句。
  • 用途:Unity协程常用于分帧执行任务、计时器和异步加载等场景,以简化异步编程和分散计算压力。
Lua协程
  • 执行环境:Lua的协程并不构成传统意义上的线程,它们是在Lua虚拟机(Lua VM)内部实现的轻量级线程。Lua协程允许在函数执行过程中挂起和恢复,从而实现复杂的流程控制和协作。
  • 状态管理:Lua协程的状态包括运行(running)、挂起(suspended)和死亡(dead)。通过coroutine.resume()和coroutine.yield()函数,开发者可以手动控制协程的状态转换。
  • 用途:Lua协程常用于优化系统并发性能、重组程序流程控制以及在高密度、高并发、高吞吐的计算场景中提升并发能力。

综上所述,Unity的协程和Lua的协程在实质上都与主线程或传统意义上的线程有所不同。Unity的协程是在主线程上执行的异步逻辑,而Lua的协程则是在Lua虚拟机内部实现的轻量级线程,用于实现复杂的流程控制和协作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值