
Lua 基础语法个人笔记
为什么学Lua
Lua是当前Unity主流的游戏热更新语言之一,依托ToLua/XLua等桥接方案,在Unity开发中有着广泛的应用和成熟的生态,对于我来说学习Lua是我学习热更新的道路上不可或缺的一环。
第一个Lua程序
-- 单行注释
--[[
多行注释_1
]]
--[[
多行注释_2
]]--
--[[
多行注释_3
--]]
print("Hello Lua!") -- 第一句Lua代码
Lua 中的变量
Lua 中的变量类型
nil – 空;
number – 数值;
string – 字符串;
bool – 布尔值;
function – 函数;
table – 表;
userdata – (可以理解为外来物的容器);
thread – 线程.
Lua 中变量的申明 与 变量种类获取
-- Lua 中申明变量不需要申明类型(类似Python 和 C#中的var 那样)
newStr = "this is a string"
-- Lua 中获取变量种类 - type()
print(type(newStr)) -- 会输出 string
-- Lua 中未被申明的变量默认为nil(空)
print(a) -- a 未在之前被申明过,这里会输出nil(空)
注意!Lua中的变量默认为全局变量,推荐申明时加上local关键字哦!!!
Lua 中 string 的操作 及 格式化输出
str = "new string"
-- 获取字符串长度
print(#str) -- 注意:一个中文字符占三个长度哦!
-- 多行打印(Lua中支持转义字符n)
print("sentence 1 n sentence 2")
-- 拼接
print(str.."string 2") -- 不同种类变量间也可以这样使用
-- 格式化输出
print(string.format("age is %d" , 20)) -- 类似于C语言
-- 转为string
print(tostring(123))
-- 大小写转换(注意:是返回一个新的string,而不是在原有string上直接进行改动)
string.upper(str)
string.lower(str)
Lua 中的运算符
算数运算符
+ - * / ^(次方)
Lua 中不支持自增自减符号及符合运算符
条件运算符
== ~= (在其他语言中是!=,注意不要搞混了!)
逻辑运算符
and or not(逻辑非);
注意:Lua 不支持 && 或 || 等!
Lua 中的条件判断语句
基本语法
Lua的if块必须有end收尾
if 条件表达式 then
-- 代码块
elseif 条件表达式 then
-- 代码块
else
-- 代码块
end
Lua 条件判断里的”真/假”规则
Lua中只有false 和 nil算”假”,0和空字符串””都表示真,一定要注意哦!
Lua 中的循环语句
循环语句基本语法
-- while 循环
local i = 1
while i <= 6 do
print(i)
until i > 3
-- repeat - until(相当于do-while)
local i = 1
repeat
print(i)
i = i + 1
until i > 3
-- for循环
-- 数值版
for i = 1,10,2 od -- 从1到10,每次加2,步长省略默认为1
print(i)
end
-- 泛型(遍历table)
-- 等我们接触到table再来看看
Lua 中也支持break跳出循环哦
Lua 中的函数
函数基本语法
-- 无参无返回值
function PrintFunc()
print("Hello Lua!")
end
-- 有参无返回值
function Func_1(a)
print(a)
end
-- 有返回值
-- 单个返回值
function Func_2(b , c)
return c + b + 1
end
-- 多返回值 (如果传入的参数和函数的不匹配,不会报错,只会补空nil 或者 丢弃)
function Func_3(a)
return a , "123" , true
end
temp , temp1 , temp2 = Func_3("a")
print(temp) -- a
print(temp1) -- 123
print(temp2) -- true
-- 函数调用
-- 示例1:
PrintFunc()
-- 示例2:
pF = PrintFunc()
pF()
函数的重载
Lua 中不支持函数的重载,如果有同名函数,只认最后一个
变长参数
function F7( ... )
-- 变长参数使用:用一个表装起来再用
arg = { ... }
for i = 1 , #arg do
print(arg[i])
end
end
F7(1 , "abc" , false , 4)
函数嵌套
function F8()
F9 = function ()
print(123)
end
return F9
end
f9 = F8()
f9() -- 123
Lua 中闭包的概念(重要!)
闭包的本质
闭包 = 函数 + 函数能”记住”的外部变量
C# 里闭包靠编译器生成匿名类来存变量,Lua更直接:每个函数都是一个闭包,能”捕获”其定义时所在作用域的所有变量.
示例:
function F9(x)
-- 改变传入参数的生命周期
return function(y)
return x + y -- 依旧还能记住x
end
end
表 – Table
数组 及 数组的遍历
申明一个数组:
arg = {1 , 2 , 3 , "asdv" , 5 , false , nil}
-- Lua 中的索引从1开始(马勒戈壁)
-- #是通用的获取长度的关键字
-- 在打印的时候,nil 会被忽略
-- 如果表 / 数组中某一位变成nil,则会影响#arg获取的长度
print(#a)
遍历数组:
for i = 1 , #arg do
print(arg[i])
end
二维数组 及 二维数组的遍历
申明一个二维数组:
a = {{1 , 2 , 3} , {"asd" , false , 1}}
遍历二维数组:
for i = 1 , #a do
b = a[i]
for j = 1 , #b do
print(b[j])
end
end
自定义索引
aa = {[0] = 1 , 2 , [-1] = 3 , 4 , nil}
print(aa[0])
print(aa[-1])
-- 自定义索引会影响用#aa获取aa数组长度
迭代器遍历
迭代器遍历主要是用来遍历表的, #得到长度,其实并不准确,一般不要用#来遍历表
a = {[0] = 1 , 2 , 3 , [-1] = 4}
-- ipairs
-- ipairs 是从1开始往后遍历,忽略小于等于0的索引 只能找到连续索引的键 如果中间断序了 也无法遍历出后面的内容
for i,k in ipairs(a) do
print(a[i])
end
-- pairs
-- 可以将所有的键都找到,通过键可以得到值
for i , v in pairs(a) do
print(a[i])
end
表 – 实现字典
字典的申明
dic = {["health"] = 100 , ["power"] = 50 , ["energy"] = 10}
-- 访问键值
print(dic["power"])
-- or:
print(a.power) -- 注意:.后不能是数字哦!
-- 删除(直接置空)
a.health = nil
字典的遍历
for key,value in pairs(a) do
print(k , value)
end
for k in pairs(a) do
print(k)
print(a[k])
end
表 – 类和结构体
Lua 中是默认没有面向对象的, 需要我们自己来实现!
Player = {
name = "player name",
health = 100,
level = 1,
-- 函数
Attack = function()
-- attack
end
}
-- 申明表过后,在表外去申明表有的变量和方法
Player.weapon = "HK416"
Player.Fly = function()
-- fly
end
注意: 表内调用表中变量或函数需要 . 出来(一定要指定是谁的,否则就是个全局变量或函数)!
-- 另一种调用方法
Player.Up = function(t)
t.level = t.level + 1
end
Player.Up(Player)
Lua 语法糖
-- 下面两个语句效果相同:
Player.Up(Player)
Player:Up() -- : 会把调用者作为第一个参数传入
-- 冒号申明
function Player:Speak()
print(self.name.."speak")
end
Lua 中有个 self 关键字代表传入的第一个值
表 – 公共操作
插入元素
local t = {10, 20, 30}
-- 在末尾插入(最常用)
table.insert(t, 40) -- {10, 20, 30, 40}
-- 在指定位置插入(后面元素自动后移)
table.insert(t, 2, 15) -- {10, 15, 20, 30, 40}
-- 注意:插入位置不能是nil或超过长度+1
移除元素
local t = {10, 20, 30, 40}
-- 移除末尾元素(最常用)
local last = table.remove(t) -- last=40, t={10,20,30}
-- 移除指定位置元素(后面元素自动前移)
local removed = table.remove(t, 2) -- removed=20, t={10,30}
-- 性能注意:移除头部或中间元素会触发元素移位,O(n)复杂度
-- 如果你频繁从头部移除,考虑用队列结构
排序
local scores = {100, 80, 120, 60, 90}
-- 默认升序
table.sort(scores) -- {60, 80, 90, 100, 120}
-- 自定义比较函数(降序)
table.sort(scores, function(a, b)
return a > b
end) -- {120, 100, 90, 80, 60}
-- 实战:按玩家等级排序
local players = {
{name = "张三", level = 10},
{name = "李四", level = 25},
{name = "王五", level = 15}
}
table.sort(players, function(a, b)
return a.level > b.level -- 等级高的在前
end)
-- 结果:李四 -> 王五 -> 张三
连接
local t = {"Hello", "World", "Lua"}
-- 简单拼接
local str = table.concat(t) -- "HelloWorldLua"
-- 带分隔符
local str = table.concat(t, ", ") -- "Hello, World, Lua"
-- 指定范围(从索引2到3)
local str = table.concat(t, ", ", 2, 3) -- "World, Lua"
-- 实战:构建CSV数据
local csvData = {
"玩家ID,等级,分数",
"1001,10,8500",
"1002,25,12000"
}
local csv = table.concat(csvData, "n")
打包
打包变长参数为表:
-- 把...参数打包成表
local function LogAll(...)
local args = table.pack(...)
print("参数个数:" .. args.n) -- 注意:args.n存的是实际个数
for i = 1, args.n do
print(args[i])
end
end
LogAll(1, 2, 3, "hello") -- 输出4个参数
解包
解包为多个值
local t = {10, 20, 30}
local a, b, c = table.unpack(t) -- a=10, b=20, c=30
-- 指定范围
local x, y = table.unpack(t, 2, 3) -- x=20, y=30
-- 实战:用数组传参调用函数
local function Add(a, b, c)
return a + b + c
end
local args = {5, 10, 15}
local result = Add(table.unpack(args)) -- 30
表的拷贝 – 浅拷贝
local original = {a = 1, b = {c = 2}}
local copy = {}
-- 方法1:手动复制
for k, v in pairs(original) do
copy[k] = v
end
-- 方法2:利用表构造器(等价)
local copy2 = {table.unpack(original)} -- 仅适用于数组
-- 问题:嵌套的表还是共享引用
original.b.c = 100
print(copy.b.c) -- 100(被影响了!)
表的拷贝 – 深拷贝
function DeepCopy(orig)
local function copy(orig, cache)
cache = cache or {}
if type(orig) ~= "table" then
return orig
end
if cache[orig] then
return cache[orig] -- 防止循环引用
end
local newTable = {}
cache[orig] = newTable
for k, v in pairs(orig) do
newTable[copy(k, cache)] = copy(v, cache)
end
return newTable
end
return copy(orig)
end
-- 实战:深拷贝配置表
local config = {
version = "1.0",
ui = {width = 1920, height = 1080}
}
local backup = DeepCopy(config)
config.ui.width = 1280
print(backup.ui.width) -- 1920(没被影响)
移动元素
local t = {10, 20, 30, 40, 50}
-- 把索引2-4的元素移到索引6开始的位置
table.move(t, 2, 4, 6) -- {10, 20, 30, 40, 50, 20, 30, 40}
清空表
local t = {a = 1, b = 2, c = 3}
table.clear(t) -- t变成空表 {}
-- 比重新分配内存更高效(复用哈希表)
多脚本执行
全局变量 & 本地变量
下面申明的变量都是全局变量:
a = 1
b = "123"
for i = 1,2 do
c = "111"
end
-- c 也是全局变量,可见这样会浪费内存
fun = function()
tt = 111
end
fun()
print(tt) -- 可见,tt也是全局变量
本地变量的关键字:
local
示例:
for i = 1,2 do
local d = i
end
print(d) -- 可以看到会输出nil,原本的d在循环体外已经失效
一般来说,不加 local 者都是全局变量
多脚本执行
关键字:
require("脚本名") -- 单引号也行
脚本中的全局变量在任何脚本中也是全局变量
如果是require加载执行的脚本,加载过一次后不会再被执行。
如果想在一个脚本中接收另一个脚本的本地变量:
-- Script_1
local a = 123
print(a)
return a -- 将本地变量返回出去
-- Script_2
b = 114514
local A = require("Script_1")
print(A) -- 123
脚本卸载
获取某个脚本是否被执行过:
package.loaded["脚本名"] -- 返回值为bool
卸载已经执行过的脚本:
package.loaded["脚本名"] = nil
大G表
大G表是一个总表(table),它将我们申明的所有全局的变量都存储于其中。
大G表的写法:
_G
示例:
for k,v in pairs(_G) do
print(k,v)
end
-- 我们申明的所有全局变量都能在_G中找到
大G表不会存本地变量哦
特殊用法
多变量赋值
普通的方法:
a = 1
b = "str"
c = 123
多变量赋值:
a , b , c = 1 , "str" , 123
-- 没被赋值到的为nil(空)
多返回值
-- 多返回值时,用多少个变量接收就有多少个值
-- 如果少了就少接,变量过多就自动补空
function Test()
return 1,2,3
end
a,b,c,d = Test()
-- a = 1 , b = 2 , c = 3 , d = nil
and or
and 和 or 不仅可以连接bool类型的变量,而且可以连接任何东西;
在lua中,只有nil 和 false 才认为是假。
短路:
print(1 and 2) -- 会打印2,这是短路现象,对于and来说只用判断第一个是否满足,就会停止计算
print(nil and 1) -- 会打印nil
print(1 or 2) -- 打印1
print(false or 2) -- 打印2
-- and-有假则假;or-有真则真
综上,可以根据and 和 or的特性来在lua中模拟三目运算符
x = 3
y = 2
local res = (x>y) and x or y
print(res) -- x
协同程序
协程的创建
协程的创建有两种方法,第一种:
fun = function()
print("coroutine")
end
co = coroutine.create(fun)
print(co) -- 可以看到,协程本质是一个线程对象(thread)
第二种:
co2 = coroutine.wrap(fun)
print(co2)
print(typeof(co2)) -- 这种方法创建的协程返回一个函数类型
协程的运行
以上两种不同的协程创建方式对应两种不同的运行方式:
coroutine.create():
coroutine.resume(co)
coroutine.wrap():
co2() -- 这种创建方式返回的是一个函数,所以直接调用函数的方法就相当于运行了一个协程
协程的挂起
fun2 = function()
while true do
print(123)
-- 协程的挂起函数
coroutine.yield()
end
end
co3 = coroutine.create(fun2)
coroutine.resume(co3) -- 可以看到这里只会打印一次 123
yield 方法可以有返回值:
coroutine.yield(1) -- 返回1
-- 启动
coroutine.resume(co3)
注意:这样写后实际上会返回两个参数:1. 协程是否启动(布尔值,也是返回的默认值);2. 括号内的参数
如果用另一种启动方式:
coroutine.wrap(co3)
-- 这样会直接返回括号中的值,不会返回bool
协程的状态
获取协程状态:
coroutine.status(协程对象)
三种协程状态:
dead -- 结束
suspended -- 暂停
running -- 进行中
结合上面的代码示例:
print(coroutine.status(co3))
另一种获取协程状态的方式:
-- 得到当前正在进行的协程的线程号
coroutine.running()
元表
元表的概念
任何表变量(table)都可以作为另一个表变量的元表;
任何表变量都可以有自己的元表(父级)。
元表的作用:当我们在“子表”中进行一些特定操作时,会执行元表中的内容。
设置元表
meta = {}
myTable = {}
-- 设置元表函数
setmetatable(myTable , meta) -- 第一个参数-子表;第二个参数-元表(父级)
特定操作
__tostring
meta = {
-- 当子表要被当作字符串使用时,会默认调用元表中的__tostring方法
__tostring = function(t)
return t.name
end,
}
myTable = {
name = "My Name"
}
setmetatable(myTable , meta)
print(myTable) -- 打印出 My Name
__call
meta = {
-- 当子表要被当作字符串使用时,会默认调用元表中的__tostring方法
__tostring = function(t)
return t.name
end,
-- 当子表被当作一个函数来使用时,会默认调用这个__call的内容
-- 当希望传参数时,要记住默认第一个参数是调用者自己
__call = function(a,b)
print(a)
print(b)
print("1")
end
}
myTable = {
name = "My Name"
}
setmetatable(myTable , meta)
myTable(123) -- 打印出 My Name n 123 n 1
运算符重载
meta2 = {
-- 加 +
__add = function(t1 , t2)
return "表相加"
end,
-- 减(__sub),乘 (__mul),除(__div),取余(__mod),幂(__pov),
-- 运算符==(__eq),小于(__lt),小于等于(__le)
-- 大于等于等需要自己取反
-- 拼接运算符..(__concat)
}
myTable2 = {}
setmetatable(myTable2 , meta2)
myTable3 = {}
print(myTable2 + myTable3) -- 打印出 “表相加”
index 和 newIndex
__index
index 当子表中找不到某一个属性时,会到元表中index指定的表去找索引:
meta6 = {}
meta6.__index = {age = 2} -- 相当于在外部申明了属性
myTable6 = {} -- 子表中无我们要找的age属性
setmetatable(myTable6 , meta6)
print(myTable6.age) -- 输出 2
寻找自身有无某个变量(不会找到元表中):
rawget(myTable6 , "age")
__newIndex
当赋值时,如果赋值一个不存在的索引,那么会把这个值赋值到newIndex所指的表中,不会修改自己
meta7 = {}
meta7.__newIndex = {}
myTable7 = {}
setmetatable(myTable7 , meta7)
myTable7.age = 1
print(myTable7.age) -- 打印 nil
print(myTable7.__newIndex.age) -- 打印 1
得到元表
getmetatable(子表)
面向对象
所有面向对象类等概念都是基于“表”来实现的
封装
Object = {}
Object.id = 33
Object.name = "Max Verstappan"
function Object:Test()
print(self.id)
print(self.name)
end
-- 冒号 是会自动将调用这个函数的对象作为第一个参数传入的写法
function Object:new()
-- self 代表的是 我们默认传入的第一个参数
-- 对象就是变量 返回一个新的变量
-- 返回出去的内容 本质上就是表对象
local obj = {}
-- 元表知识复习:__index 当找自己的变量时如果找不到,就会去找元表当中__index指向的内容
self.__index = self
setmetatable(obj, self)
return obj
end
local myObj = Object:new()
print(myObj.id)
myObj:Test() -- 打印 1
myObj.id = 2
myObj:Test() -- 打印 2
继承
延续上一部分的代码
function Object:subClass(className)
-- _G知识点:_G是总表,所有声明的全局变量,都以键值对的形式存在其中
_G[className] = {}
-- 实现继承规则
local obj = -G[className]
self.__index = self
-- 子类定义一个 base 属性来代表父类
obj.base = self
setmetatable(obj , self)
end
Object:subClass("Driver")
local p1 = Person:new()
print(p1.id)
多态
(依旧继承上一部分的代码)
复习一下多态的意义:相同行为不同表象
Object:subClass("GameObject")
GameObject.posx = 0
GameObject.posy = 0
function GameObject:Move()
self.posx = self.posx + 1
self.posy = self.posy + 1
print(self.posx)
print(self.posy)
end
GameObject:subClass("Player")
function Player:Move()
-- 避免吧基类表 传入到方法中 这样相当于公用一张表的属性了
-- 如果要执行父类逻辑,不要直接使用冒号调用
-- 而是要通过 . 调用,然后自己传入第一个参数
self.base.Move(self)
end
local p1 = Player:new()
p1:Move()
自带库
这里仅给出几个常用的库
时间
-- 系统时间
print(os.time())
print(os.time({year = 2026 , month = 9 , day = 5}))
-- 日期
local nowTime = os.date("*t")
for k,v in pairs(nowTime) do
print(k,v)
end
print(nowTime.hour)
数学运算
-- 绝对值
print(math.abs(-44))
-- 弧度转角度
print(math.deg(math.pi / 2))
-- 三角函数(举例:cos)
print(math.cos(math.pi)) -- 输出 -1
-- 向下 向上 取整
print(math.floor(1.5))
print(math.ceil(1.8))
-- 最大值 最小值
print(math.max(1,2))
print(math.min(2,3))
-- 小数分离 分成整数和小数部分
print(math.modf(1.2))
-- 幂运算
print(math.pow(2,5))
-- 随机数
-- 先设置随机数种子
math.randomseed(os.time())
print(math.random(100))
-- 开方
print(math.sprt(4)) -- 2
-- 还有其他方法这里不展示了
路径
-- lua 脚本加载路径
print(package.path)
package.path = package.path .. ";C:
"
Lua 垃圾回收
垃圾回收关键字:collectgarbage
-- 获取当前lua占用内存数
print(collectgarbage("count"))
-- 进行垃圾回收
collectgarbage("collect")
print(collectgarbage("count"))
-- 释放内存空间,可以直接置空 null
-- 在Unity中热更新开发,尽量不要使用自动垃圾回收
