Lua 基础语法个人笔记
本文最后更新于5 天前,其中的信息可能已经过时,如有错误请发送邮件到1077511618@qq.com

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中热更新开发,尽量不要使用自动垃圾回收
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇