Loading

Lua语法入门

基础特性

lua中不需要分号作为语句的结束点

注释语句

-- 单行注释

--[[
 注释语句
]]--

..操作符可以用于拼接类型,拼接后的结果是stringnil不支持拼接)

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)
    
  • 与,或和非的操作符是andornot。短路操作lua中仍然存在

  • lua中不支持位运算符,不支持三目运算符。但可以使用andor来实现三目运算符的功能

    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)

局部变量与全局变量

默认声明的变量是全局变量,以下变量wordscoredata都是全局变量。全局变量的生命周期是整个程序的执行周期

word = "Hello"

for i = 1, 10, 1 do
    score = 10
end

function test_func()
    data = 300
end
test_func()

所有的全局变量都存储在G表中,包括tablefunction等等

自然也可以通过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)

使用rawgetrawset方法来限制只在当前表中查找或添加

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"):手动进行垃圾回收
posted @ 2022-01-03 20:51  _FeiFei  阅读(574)  评论(1编辑  收藏  举报