Lua
作为简单的脚本语言,Lua学习成本极低(无论是基础和高级特性),但是因为过于自由导致其itellisense支持不好,很多时候你得嗯搜某个变量或者通过变量的方法或成员来间接定位...
不过也很正常,有些变量可能是运行时生成的...
但是在游戏开发中,Lua是必不可少的,它的作用是运行时调用引擎的一些东西实现某些逻辑或者控制界面逻辑。OK,下面是Lua的一些基础特性。
哦对,我希望你懂C/C++,我会拿它们来对比。
Variable
Lua有好几种基本类型,分别是
- nil:表示一个无效值,通常用来表示变量未初始化、未定义或已释放。
-
boolean:表示逻辑上的 true 或 false。
-
number:表示双精度浮点数。Lua 数字类型采用 IEEE 754 标准,可以支持非常大或非常小的数字,但不支持整型。
-
string:表示字符串。Lua 中的字符串可以使用单引号、双引号或者长括号进行定义。
-
function:表示函数。在 Lua 中,函数也是一种类型,并且可以赋值给变量、作为参数传递或者返回值。
此外还有local关键字。在 Lua 中,local 是一个关键字,用于声明局部变量。使用 local 定义的变量只在定义它的块中可见。这样做可以避免命名冲突和意外的变量覆盖。
在 Lua 中,当定义一个新变量时,如果没有使用 local 关键字,则该变量默认为全局变量,会被添加到 _G 全局表中,并且可以从任何位置访问和修改该变量。这可能会导致一些潜在的问题,比如可能会被其他地方意外地修改或者覆盖等。
Number
无论是浮点数和整数,在lua里面都被称为number
local num1 = 10
local num2 = 10.0
print("result: ", assert(type(num1) == type(num2)))
String
lua使用两个双引号代表string
local str = "hello string"
字符串的连接是简单的,只需要两个点就好
local name = "lua"
print("hello " .. name)
下面介绍几种常见的方法
local str = "hello lua"
string.len(str) -- 返回长度
string.find(s, pattern, [, index[, plain]]) -- 寻找字串,类似于c++的search和c的strstr,index是从哪里开始寻找,plain指示是否关闭转义
string.lower(str)
string.upper(str)
-- 以上是字符串转小写/大写
string.format(fmt, ...) -- 类似于std::format
string.sub(str, beginidx[, endidx]) -- 类似于substring
string.rep(str, n)-- 返回重复str串n次的字符串
string.reverse(str)-- 字符串反转
还有两个g开头的函数,它们涉及正则表达式
local str = "The quick brown fox jumps over the lazy dog"
-- 使用 gsub 替换所有匹配的单词
local newStr1, count1 = string.gsub(str, "%w+", "cat")
print(newStr1) --> "cat cat cat cat cat cat cat cat cat"
-- 使用 gsub 替换前两个匹配的单词
local newStr2, count2 = string.gsub(str, "%w+", "cat", 2)
print(newStr2) --> "cat cat brown fox jumps over the lazy dog"
-- 使用 gmatch 遍历所有匹配的单词
for word in string.gmatch(str, "%w+") do
print(word)
end
Table
table是一个类似于std::map的结构,不同点是table不能自动排序,table可以指定内存是否连续
首先是不连续内存的table, 这种table也是由一个个pair组成的,pair类型是\<string, any>,在下面的示例中,左侧的值没有string的标志引号但是lua会自动把它们转化为string
这里点和直接用中括号访问都是OK的
tb = {
-- key = value
lua = "noob",
cpp = "master",
java = "wtf",
}
print(tb["lua"]) -- ok
print(tb.lua) -- ok
下面是连续内存的table写法,它们连续地从下标1到排布,你可以通过\<number, any>设置它
tb = {1, [2] = 2}
print(tb[1], tb[2])
Scope
首先是如何写分支即if-else语句,在Lua内是这样实现的
local a = 0
local b = 0
if a ~= b then
print("not equal")
else
print("equal")
end
下面是使用elseif来代替else
local a = 0
local b = 0
if a ~= b then
print("not equal")
elseif a == b then
print("equal")
end
Lua不存在switch结构
下面是一些循环的代码块,首先展示基本的for循环
for i = 1, 10 do
print (i)
end
和python一样,第三个参数是step
for i = 10, 1, -1 do
print (i)
end
lua有C++一样range-based loop(好用的语言好像都有)
arr = {0, 1, 2, 3, 4, 5}
for _, i in pairs(arr) do
print(i)
end
这里请注意,花括号代表着一种lua的基本类型table,用法很像c++的map(但是不会自动排序),遍历table会返回key和value(使用pairs函数),其中这里的Key是下标,不过Lua的下标是从1开始的。
所以我们只想要value而不是key,所以遍历arr只需要把第一个值屏蔽掉就好了。
我们也可以通过下标来访问arr。
arr = {0, 1, 2, 3, 4, 5}
for i = 1, #arr do
print(arr[i])
end
这里#用于取得连续下标(不显式指定key时)的table的长度,对于显示指定的必须遍历求值
arr = {
zero = -1,
one = 0,
two = 1,
three = 2,
}
count = 0
for _, __ in pairs(arr) do
count = count + 1
end
print(count)
跑题了... 然后是while和do...while结构,这个在lua中也有
local i = 0
while i <= 10 do
print(i)
i = i + 1
end
do...while结构也没啥,就是给重命名了一下
local i = 0
repeat
print(i)
i = i + 1
until i > 10
顺便提一嘴goto,因为Lua只有break没有continue,我会使用goto代替continue
for i = 1, 10 do
if i % 2 == 1 then
goto loopend
else
print(i)
end
::loopend::
end
Function
Lua的function大多数以这样的形式定义
function func()
print("hello lua")
end
等价于
func = function() print("hello lua") end
对于表而言,我们可以将它看作是一个类,我们尝试写一个tableMaker
tableMaker = {
new = function()
return {}
end
}
m_table = tableMaker.new()
print(type(m_table))
还有一种带冒号的函数声明,就是
function tableMaker:func()
-- ...
end
它等价于
function tableMaker.func(self)
end
因为上述等价关系,我们可以这样写以实现分离
tableMaker = {
new = {}
}
function tableMaker.new()
return {}
end
m_table = tableMaker.new()
print(type(m_table))
我们可以实现带参的new
tableMaker =
{
new = {},
}
function tableMaker.new(params)
return { name = params, }
end
m_table = tableMaker.new("Loris")
print(m_table.name)
对于变长参数和递归,Lua也是支持的
function xadd(a, ...)
if select("#", ...) == 0 then
return a and a or 0
end
return a + xadd(...)
end
print(xadd(1, 2, 3, 4, 5)) --15
print(xadd(1)) -- 1
print(xadd()) -- 0
对于这里的逻辑运算符 a and b or c,其实就是我们在C++里的三目
a ? b : c
注意在Lua中,只有nil和false被视为false,而0是true
这里还有一个trick就是lua的函数不支持默认参数,就像下面这样
void hello(const char* msg = "cpp"){
std::printf(msg);
}
hello(); // ok
我们可以对以上的求和做一下改变
function xadd(a, ...)
a = a or 0
if select("#", ...) == 0 then
return a
end
return a + xadd(...)
end
我们在C++里有这样的函数std::apply,在Lua里当然也可以,不过这里就不再是tuple而是table,此外函数也不再是apply而是table.unpack,注意这里的table必须是连续内存的。
function xadd(a, ...)
if select("#", ...) == 0 then
return a and a or 0
end
return a + xadd(...)
end
local tb = {1, 2, 3, 4, 5}
print(xadd(table.unpack(tb)))
Metatable
Metatable是另一个技巧,一般用于实现类似OOP的功能
在这里注意的是,Metatable只是一类特殊的表,没有什么特别的
我们写一个
PersonMt = {}
PersonMt.__index = PersonMt
function PersonMt:Say()
print(self.name)
end
Person = {
new = function(name)
return setmetatable({name = name}, PersonMt)
end
}
me = Person.new("Dululu")
me:Say()
这里有两个新东西,一个是setmetatable(target, meta),即把meta设为target的元表
当调用me:Say()时,首先看me内有没有成员Say,发现没有,此时需要访问__index,注意到我们已经对me绑定了元表,我们会访问元表的__index. 这个__index绑定的是元表本身,就会向这个元表寻找[,...内有没有成员Say,发现没有,此时需要访问__index,注意到我们已经对...绑定了元表,我们会访问元表的__index.]...。
__index的说明是这样的,他有两个参数,一个是当前表,另一个是key。当对一个表执行这样的操作时,例如tb[key],如果tb没有对应的key,就会调用这个方法。与它相似的,对于操作__newindex,是当对一个表执行这样的操作时,例如tb[key] = value,如果tb没有对应的key,就会调用这个方法,这个有三个参数,即除了__index的两个参数外,额外加入了value。
__index默认是返回nil,也可以自定义。一个简单的定义如下
function t:__index(key)
rawset(self, key, "Default Value")
return "Default Value"
end
function t:__newindex(key, value)
rawset(self, key, value)
return
两个函数的意思分别是,如果不存在,则创建一个默认值;直接赋值。注意rawget不会触发__index,rawset不会触发__newindex.
注意到还可以嵌套元表,一个简单的实现是这样的
PersonMtMt = {}
PersonMtMt.__index = PersonMtMt
PersonMt = {}
PersonMt.__index = PersonMt
Person = {
new = function(name)
return setmetatable({name = name}, PersonMt)
end
}
function PersonMt:Say()
print(self.name)
end
function PersonMtMt:Bark()
print(self.name, "!!!")
end
setmetatable(PersonMt, PersonMtMt)
me = Person.new("Dululu")
me:Say()
me:Bark()
Coroutine
与协程的交互就像打乒乓球,你把信息给协程,协程运行,协程得到结果再返回给主协程,如此反复
我们写一个代码作为示例
function co_print(msg) -- 协程函数,特征是有yield,在C++里有co_yield的函数被视为协程函数
print(msg)
while true do
isend, msg = coroutine.yield("done coroutine"); -- 协程把值(这里是done coroutine)yield回去,并且准备接受两个新值(isend, msg)
print(msg)
if isend then
break
end
end
return "bye coroutine"-- 最终返回值
end
co = coroutine.create(co_print) -- 创建协程,但是一开始并不会主动运行
coroutine.resume(co, "hello coroutine") -- 运行协程函数,并且参数是hello coroutine,这里是一个参数,对应函数参数
for i = 1, 10 do
_, logx = coroutine.resume(co, i == 10 ,"--------") -- 运行协程函数,参数为两个,对应yield后准备接受的值,这里resume第一个返回值是个boolean值,含义如下
-- 当协程成功启动并继续执行时,该布尔值为true。
-- 如果协程被唤醒后出现任何错误或异常情况时,则该布尔值为false。
print(logx)
end
我们前面说到的乒乓球模型,其实有一些特别的地方,就在函数的开始和结束。因为当一个函数也需要参数启动或需要返回时,情况有些不同,但是和yield一个值回去没什么区别。
这显然不够简单,Lua为了使得协程用起来更简单,设计了coroutine.wrap,它可以直接调用而不是使用coroutine.resume,而且不会返回额外的boolean值
function co_print(msg)
print(msg)
while true do
isend, msg = coroutine.yield("done coroutine");
print(msg)
if isend then
break
end
end
return "bye coroutine"
end
co = coroutine.wrap(co_print)
co("hello coroutine")
for i = 1, 10 do
logx = co(i == 10 ,"--------")
print(logx)
end
Load/Loadfile
这个太强了...我把它认为是超强宏,后者可以以为是文件读取版本的Load
举个例子
local str = "print('Hello, world!')"
local func, err = load(str)
if func ~= nil then
func()
else
print("Error:", err)
end
他会把这个语句包装为一个函数,如果它是ok的(没语法错误),就可以正确运行否则会打印出错误
最骚的是可以配合string.format各种自定义...
例子就不举了,太多了...