Lua语法入门
基础特性
lua中不需要分号作为语句的结束点
注释语句
-- 单行注释
--[[
注释语句
]]--
..
操作符可以用于拼接类型,拼接后的结果是string
(nil
不支持拼接)
a = 123 .. 456
-- data type is string
-- data is 123456
print("data type is " .. type(a) .. "\n" .. "data is " .. a)
#
代表获取长度,例如可以获取字符串或者数组等的长度。获取的结果是number
类型
name = "Jelly"
-- 5
print(#name)
使用#
获取长度的规则是:末尾的nil不列入长度计算
a = {1, 2, nil, 3, 4, 5, nil, nil, nil}
-- 6
print(#a)
常用函数
输出语句
print("Hello World")
获取变量类型,将返回变量类型名的string
type()
类型系统
lua中并不需要声明变量类型,它是弱类型的语言
对于空数据而言,它们的类型是nil
a = nil
-- nil
print(type(a))
对于数值而言,它们的类型是number
a = 20.21
-- number
print(type(a))
对于字符串而言,它们的类型是string
,不管是单引号或者是双引号
a = 'Hello'
-- string
print(type(a))
对于真假而言,它们的类型是boolean
a = true
-- boolean
print(type(a))
lua中使用没有声明过的变量,不会报错,默认是nil
-- nil
print(magicData)
字符串常见操作
字符串拼接
- %d:代表与
number
拼接 - %a:代表与任意字符拼接
- %s:代表与
string
拼接
-- I am No.1
print(string.format("I am No.%d", 1))
其他类型转化为字符串
a = true
b = tostring(a)
其他常用方法
大小写转换,以及倒序字符串
-- HELLO WORLD
print(string.upper("hello world"))
-- hello world
print(string.lower("HELLO WORLD"))
-- OLLEH
print(string.reverse("HELLO"))
查找截取以及修改。lua中字符串从1开始计数,而不是0
-- 查找字符串 a的值为2
a = string.find("Hello", "ell")
-- llo
print(string.sub("Hello", 3, 5))
a = string.gsub("Hello World", "o", "O")
-- HellO WOrld
print(a)
ASCII码互转
-- 将"L"转化为ASCII码
a = string.byte("L")
-- a的值为76 a的类型是number
print(type(a))
-- 将a从ASCII转换为字符串 结果是"L"
print(string.char(a))
运算符
-
lua中没有自增自减运算符,即
--
,++
-
lua中没有符合运算符,即
-=
,+=
-
string
类型可以进行运算符操作,会自动转成number
,运算的结果也是number
a = "3.14" + 2
-
由于lua中的数都是number类型,没有整形和浮点型之分,因此
-- 0.5 print(1 / 2)
-
lua中有幂运算符号
-- 8.0 print(2 ^ 3)
-
lua中的不等于运算符是
~=
-- true print(1 ~= 20)
-
与,或和非的操作符是
and
和or
和not
。短路操作lua中仍然存在 -
lua中不支持位运算符,不支持三目运算符。但可以使用
and
和or
来实现三目运算符的功能i, j = 10, 30 -- result的值是30 result = (i > j) and i or j
if语句
a = 20
if a > 10 and a < 30 then
print("a > 10 and a < 30")
end
a = 100
if a == 20 then
print("a == 20")
elseif a == 30 then
print("a == 30")
else
print("a ~= 20")
print("a ~= 30")
end
switch语句
lua中没有switch
语句
循环语句
while循环
输出01234
num = 0
while num < 5 do
print(num)
num = num + 1
end
do-while循环
输出012345
num = 0
repeat
print(num)
num = num + 1
until num > 5
for循环
lua中的for循环语句会默认进行+1
操作,且循环终止的条件是<=
-- 输出 0 1 2 3 4 5
for i = 0, 5 do
print(i)
end
以上代码相当于
for (int i = 0; i <= 5; i++)
std::cout << i << std::endl;
如果不想采用默认的+1
操作,可以显式指明
-- 输出 0 2 4
for i = 0, 5, 2 do
print(i)
end
以上代码相当于
for (int i = 0; i <= 5; i += 2)
std::cout << i << std::endl;
如果想采用递减操作,也是显式指明的操作
-- 输出 5 4 3 2 1 0
for i = 5, 0, -1 do
print(i)
end
函数
无参数无返回值
以下代码展示函数的声明和调用,与C++相同,声明和调用的顺序是固定的
function func1()
print("this is func1")
end
func1()
除此之外还可以有类似“匿名函数”的写法
func2 = function()
print("this is func2")
end
func2()
带参数函数
同理,lua是弱类型的语言,因此函数参数不需要声明类型
function func1(a)
print(a)
end
func1(20)
更重要的,如果参数数量不匹配,那么会自动补全或丢弃
function func1(a, b)
print(b)
end
-- 补全两个nil 输出nil
func1()
-- 丢弃末尾两个参数 输出30
func1(20, 30, 430, 5)
函数返回值
由于是弱类型语言,因此不需要指明返回值类型
function cal(a, b)
return a * b
end
print(cal(2, 3))
多返回值的处理情况
function cal(a, b)
return a + b, a - b, a * b
end
data1, data2, data3 = cal(2, 3)
print(string.format("%d %d %d", data1, data2, data3))
多返回值的情况可以参照C++17中的结构化绑定。同样的多余的返回参数如果没有变量接住,那么也会被丢弃;如果声明了多个变量但返回的元素个数不够,那么会默认置空
template<typename T>
std::tuple<T, T, T> cal(T a, T b) {
return {a + b, a - b, a * b};
}
int main() {
auto [data1, data2, data3] = cal(2, 3);
std::cout << data1 << " " << data2 << " " << data3 << std::endl;
}
函数的隐藏
由于Lua中不存在函数重载,因此函数将会出现重定义的情况(即隐藏先声明的函数)
function func(a)
print(a)
end
function func()
print("empty func")
end
-- 参数100被丢弃 然后输出empty func
func(100)
变长参数
function func(...)
table = {...}
for i = 1, #table, 1 do
print(table[i])
end
end
func(100, 200, "Hello", true, 3.14)
换做C++的写法那就是
template<typename... Ts>
void func(Ts... args) {
((std::cout << args << std::endl), ...);
}
int main() {
func(100, 200, "Hello", true, 3.14);
}
函数嵌套
形成一个闭包,变量x的生命周期延长
function func(x)
return function(y)
return x + y
end
end
funcObj = func(5)
-- 输出15
print(funcObj(10))
由于C++中需要手动管理对象的生命周期,按引用捕获并不会延长变量的生命周期,所以只能使用按值捕获,因此C++的写法是
auto func(int x) {
return [=](int y) { return x + y; };
}
int main() {
// 存在捕获 所以使用auto
auto funcObj = func(5);
std::cout << funcObj(10) << std::endl;
}
表
table
是一个数据类型,不论是数组,字典或者是类等等,它们的类型都是table
数组
如下图即声明了一个数组,lua中的数组并不要求数组内元素类型的一致
a = {1, false, 3, "123"}
lua中下表索引是从1开始的,通过[]
对数组中元素进行访问。lua中数组越界并不会导致程序崩溃,它会返回nil
-- false
print(a[2])
-- nil
print(a[100])
二维数组
a = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} }
for i = 1, #a do
for j = 1, #a[i] do
print(a[i][j])
end
end
自定义索引
lua中可以自定义数组的索引,如下图代码,令索引1的值为1,索引2的值为2,索引3的值为3,索引4的值为4
a = {[1] = 1, [2] = 2, [3] = 3, [4] = 4}
lua中可以跳跃的指定索引,被忽略的索引在访问时值为nil
a = {[1] = 1, [2] = 2, [3] = 3, [6] = 4}
-- nil
print(a[5])
因此在此基础上就可以实现所谓“哈希表”
a = {["name"] = "Jelly", ["age"] = 20, ["sex"] = 0}
-- age 20
-- name Jelly
-- sex 0
for k, v in pairs(a) do
print(k, v)
end
-- 直接新增元素
a["score"] = 100
-- 访问新增加的元素
print(a["score"])
ipairs和pairs迭代器遍历
ipairs
适合访问连续的非空数组pairs
能够访问所有表,“哈希表”只能通过pairs
访问
a = {[0] = 0, 1, 2, [-1] = -1, [5] = 5}
-- 使用ipairs迭代器只能访问到数组中的元素1和元素2
-- ipairs遍历将会从1开始往后遍历 且只能访问连续索引的数据 遇到断序后无法继续往后访问
for k, v in ipairs(a) do
print("key:" .. k .. " value:" .. v)
end
print()
-- 使用pairs迭代器能够访问数组中的所有元素
for k, v in pairs(a) do
print("key:" .. k .. " value:" .. v)
end
至此我们可以将其理解为是C++中的std::unordered_map
,然后通过std::pair
去对键值对进行访问
使用表实现面向对象
本质上还是“哈希表”,只是不同的语法体现形式
Student = {
age = 20,
score = 100,
sayHello = function()
print("Hello")
end
}
print(Student.score)
Student.sayHello()
与“哈希表”类似,可以在表的外部添加“类成员”和“类成员函数”
Student = {
sayHello = function()
print("Hello")
end
}
Student.score = 100;
Student.sayGoodBye = function()
print("GoodBye")
end
如果想要在表中的函数对象中访问表中的元素,有两种方法
-
通过指定变量名进行访问
Student = { age = 20, printAge = function() -- 需要指明是Student中的值 否则将会使用全局变量 若不存在此全局变量 则默认为nil print(string.format("Age is %d", Student.age)) end } Student.printAge()
-
通过传递函数参数进行访问
Student = { score = 100, printScore = function(s) print(string.format("Score is %d", s.score)) end } -- 调用时传递自己 Student.printScore(Student)
通过语法糖
:
进行优化Student = { score = 100 } function Student:printScore() -- : 代表函数默认第一个参数会传递“self” -- self搭配:使用 代表使用传入的第一个参数 print(string.format("Score is %d", self.score)) end -- : 代表调用函数时 会默认传一个自己(Student)作为第一个参数 Student:printScore()
这与C++中的运作方式类似,C++在调用非静态成员函数时,编译器会默认添加
this
指针作为函数的第一个参数。只不过在Lua中我们需要显示指明传入一个self
表的常见操作
插入
表的插入操作有两个版本的“重载”。分为指定位置与不指定位置
-- 在第五号元素的前面插入值"Hello"
a = { 20, 30, 40, 50, 90 }
table.insert(a, 5, "Hello")
-- 不指定位置 在表的末端插入
a = { 20, 30, 40, 50, 90 }
table.insert(a, 10)
删除
表的删除操作也有两个版本的“重载”。分为指定位置与不指定位置
-- 删除表中第三个元素 即40
a = { 20, 30, 40, 50, 90 }
table.remove(a, 3)
-- 不指定位置 删除最后一个元素
a = { 20, 30, 40, 50, 90 }
table.remove(a)
排序
-- 降序排序
a = { 20, 30, 40, 50, 90 }
table.sort(a, function(a, b)
return a > b
end)
局部变量与全局变量
默认声明的变量是全局变量,以下变量word
,score
,data
都是全局变量。全局变量的生命周期是整个程序的执行周期
word = "Hello"
for i = 1, 10, 1 do
score = 10
end
function test_func()
data = 300
end
test_func()
所有的全局变量都存储在G表中,包括table
,function
等等
自然也可以通过G表来访问其中的全局元素
globalData = 20
print(_G["globalData"])
局部变量需要使用local
修饰。局部变量的生命周期是当前作用域或当前文件,离开了作用域或离开了文件访问将得到nil
function test_func()
local data = 300
end
多脚本执行与卸载
使用require
关键字执行其他脚本文件中的代码
-- HelloWorld.lua
print("Hello World")
require("AnotherCode")
print("Hello World")
-- AnotherCode.lua
print("AnotherCode")
在运行HelloWorld.lua
时
- 输出
HelloWorld
- 执行
AnotherCode.lua
文件,输出AnotherCode
- 输出
HelloWorld
一个脚本文件只能被加载一次,即多次require
一个脚本文件没有效果。我们需要卸载脚本文件
-- 输出脚本有没有被加载 返回一个boolean
print(package.loaded["AnotherCode"])
-- 卸载脚本
package.loaded["AnotherCode"] = nil
脚本文件可以有一个返回值,然后再另一个文件中通过require
来获取
-- HelloWorld.lua
data = require("AnotherCode")
print(data)
-- AnotherCode.lua
return 10 + 20
协程
创建协程
-
创建协程对象并运行
co = coroutine.create(function () for i = 1, 10, 1 do print("Hello" .. i) end end) coroutine.resume(co)
-
创建协程函数并运行
coFunc = coroutine.wrap(function () for i = 1, 10, 1 do print("Bye" .. i) end end) coFunc()
在执行协程的时候,执行权转移到协程中,只有当协程执行完毕或主动挂起时,执行权才会转移回来
挂起协程(yield)
co = coroutine.create(function ()
for i = 1, 10, 1 do
print("Hello" .. i)
coroutine.yield()
end
end)
-- 共计会执行四次Hello 由于协程被挂起 因此需要重复调用以让协程从挂起点恢复执行
coroutine.resume(co)
coroutine.resume(co)
coroutine.resume(co)
coroutine.resume(co)
协程返回值
coroutine.yield()
的函数参数可以作为返回值在外部获取
-
协程对象的返回值
可以看作是
std::tuple<bool, Ts...>
,第一个参数代表协程是否被成功执行co = coroutine.create(function () for i = 1, 10, 1 do print("Hello") coroutine.yield(i, 10 - i) end end) -- true 1 9 isWork, first, second = coroutine.resume(co) print(isWork, first, second)
-
协程函数的返回值
可以看作是
std::tuple<Ts...>
,没有默认参数coFunc = coroutine.wrap(function () for i = 1, 10, 1 do coroutine.yield(i) end end) -- 1 print(coFunc())
协程传参
- 当首次执行协程时,传递的参数是执行函数的参数
- 当协程从挂起点恢复执行时,传递的参数是
coroutine.yield()
的返回值
协程状态
只能查看协程对象的状态,协程函数的状态无法查看
suspended
:代表协程被挂起(当一个协程被创建但是没有被执行时,它是suspended
的)running
:代表协程正在运行dead
:代表协程已经执行完毕normal
:当协程A中启动协程B后,在执行协程B的过程中,协程A的状态是normal
co = coroutine.create(function ()
for i = 1, 10, 1 do
coroutine.yield()
end
end)
coroutine.resume(co)
-- suspended
print(coroutine.status(co))
八股文题目
function foo (a)
print("foo", a)
return coroutine.yield(2 * a)
end
co = coroutine.create(function (a,b)
print("co-body", a, b)
local r = foo(a + 1)
print("co-body", r)
local r, s = coroutine.yield(a + b, a - b)
print("co-body", r, s)
return b, "end"
end)
print("main", coroutine.resume(co, 1, 10))
print("main", coroutine.resume(co, "r"))
print("main", coroutine.resume(co, "x", "y"))
print("main", coroutine.resume(co, "x", "y"))
执行结果为
co-body 1 10
foo 2
main true 4
co-body r
main true 11 -9
co-body x y
main true 10 end
main false cannot resume dead coroutine
元表
- 任何一个表都可以设置为另一个表的元表(类似父类的概念)
- 对子表进行特定操作时,会执行元表中“重写”的操作。调用的前提是:它是一个表的元表 且 它“重写”了相应的操作
base_student_table = {}
student_table = {}
-- 将base_student_table设置为student_table的元表
setmetatable(student_table, base_student_table)
__tostring
当表需要当作string
类型使用时,会默认调用元表中的__tostring
函数。__tostring
默认会传递一个调用者作为形参
base_student = {
__tostring = function(a)
return a.name
end
}
student = {
name = "Jelly",
}
setmetatable(student, base_student)
-- Jelly
print(student)
这其实有一点类似C++中的类型转换重载。只是C++中的类型转换重载不允许有返回值与形参
struct base_student {
operator std::string() {
return "Jelly";
}
};
struct student : base_student {};
__call
当表需要当作function
使用时,会默认调用元表中的__call
函数。__call
默认会传递一个调用者作为形参
base_student = {
__call = function(default, param)
print(default.name .. param)
end
}
student = {
name = "Jelly",
}
setmetatable(student, base_student)
-- Jelly is here
student(" is here")
这其实相当于C++中的仿函数
运算符重载
- 加号运算符重载:
__add
;减号运算符重载:__sub
;乘法运算符重载:_mul
;除法运算符重载:__div
- 取余运算符重载:
__mod
;求幂运算符重载:__pow
- 比较运算符重载:
__eq
;小于运算符重载:__lt
;小于等于运算符重载:__le
- 没有大于和大于等于的运算符重载,lua会自动对小于等于和小于进行取反
base_student = {
__add = function(left, right)
return left.score + right.score
end
}
student1 = {
score = 100
}
student2 = {
score = 200
}
setmetatable(student1, base_student)
setmetatable(student2, base_student)
-- 300
print(student1 + student2)
__index与__newindex
当表中找不到某个数据项时,会通过它的元表的__index
所指定的表中去搜索
base_student = {
base_student.__index = { name = "Jelly" }
}
student = {}
setmetatable(student, base_student)
print(student.name)
额外注意,当元表的__index
指向的是自己时,__index
需要写在表外部
base_student = {
name = "Jelly"
}
base_student.__index = base_student
当表需要修改某数据项时,且该数据项不存在于当前表中,会修改(添加)它元表的__newIndex
所指定的表
student_db = {}
base_student = {}
base_student.__newindex = student_db
student = {}
setmetatable(student, base_student)
-- 添加到student_db中
student.score = 200;
print(student_db.score)
使用rawget
和rawset
方法来限制只在当前表中查找或添加
print(rawget(student, "name"))
rawset(student, "name", "Jelly")
Lua实现面向对象
利用元表和__index
查找索引来模拟面向对象
利用表模拟一个Object基类
Object = {
id = 0
}
Object.printID = function(self)
print(self.id)
end
Object.new = function(self)
-- 创建一个局部变量
local obj = {}
-- 将调用者设置为obj的元表
setmetatable(obj, self)
-- “子类中找不到在基类中查找”
self.__index = self
return obj;
end
Object.createSubClass = function(self, subClassName)
-- 在G表中创建全局对象 作为“类型”
_G[subClassName] = {}
local subClass = _G[subClassName]
-- 该类型的“父类”是调用者
setmetatable(subClass, self)
subClass.base = self
self.__index = self
end
在Object
的基础上创建子类GameObject
,子类拥有属性scale
,以及两个“成员函数”
Object:createSubClass("GameObject")
GameObject.scale = 1.0
GameObject.addScale = function(self, multi)
self.scale = self.scale * multi
end
GameObject.printScale = function(self)
print(self.scale)
end
在GameObject
的基础上创建子类Person
,“重写”了方法addScale
GameObject:createSubClass("Person")
Person.addScale = function(self, multi)
self.base.addScale(self, multi)
print("Person Add Scale")
end
测试代码,通过:new
创建两个局部对象
local p1 = Person:new()
p1:addScale(20)
-- 20.0
p1:printScale()
local p2 = Person:new()
p2.scale = 200
-- 200
p2:printScale()
垃圾回收
Lua中有自动定时垃圾回收机制,也可以手动执行
print(collectgarbage("count"))
:打印Lua程序占用的内存空间大小,单位是KB- collectgarbage("collect"):手动进行垃圾回收