【Lua基础】基本语法

注释

Lua使用的注释如下:

-- 单行注释使用
--[[
    多行注释使用
    多行注释使用
]]

跟其它语言使用的注释稍有不同,它以"--"为注释的标记。

类型

Lua是动态类型语言,即变量的类型可以变。

通过type可以测试给定变量的类型,下面是例子:

print(type("helloworld"))           --> string
print(type(1))                      --> number
print(type(nil))                    --> nil
print(type(true))                   --> boolean
print(type(type))                   --> function
print(type({x = 0, y = 1}))         --> table
co = coroutine.create(function()
    print("helloworld")
end)
print(type(co))                     --> thread

上面展示了Lua的7个基本类型:

string/number/nil/boolean/function/table/thread

另外还有一个类型是userdata,还不知道如何展示......

几点说明:

  1. nil类型只有nil这么一个值。
  2. boolean有两个值true和false。另外Lua中的所有值都可以用在条件语句中,且除了false和nil表示假,其它都表示真。比如0,它表示真。
  3. 数值类型只有number,没有int、float等类型。
  4. string可以用双引号,也可以用单引号指定。还可以用[[里面是字符串]],[[]]的特点是可以包含多行,可以嵌套,且不解释转移字符。
  5. function和其他上述的类型一样,属于第一类值,就是说也可以存在普通的变量里面。
  6. table、userdata和thread后面还会讲。

变量

变量不需要声明。

给一个变量赋值后就生成了一个全局变量。

print(a)    --> nil
a = 0
print(a)    --> 0
a = "hello world"
print(a)    --> hello world
a = nil     --> 相当于删除a这个变量

没有初始化的全局变量也可以访问,得到的是nil。实际的意思是,如果一个变量不是nil,就表示变量存在。所以如果给一个变量赋值为nil,就表示删除这个变量。

使用local关键字可以创建局部变量。

do
    local b = 0
    print(b)    --> 0
end
print(b)        --> 打印nil,因为a的生命周期已经结束

局部变量只在被声明的那个代码块内有效。代码块可以是一个控制结构,一个函数体,或者一个chunk(变量被声明的那个文件或者字符串)。

Lua中可以使用多变量赋值:

a,b,c = 1,"helloworld"
print(a)    --> 1
print(b)    --> helloworld
print(c)    --> nil

多变量赋值也不是很讲究,=右边如果比左边少,则相应的变量为nil,比如这里的c;=右边如果比左边少,则多出来的赋值会被忽略。

多变量赋值在函数返回时也很有用,因为Lua中的函数是可以返回多个值的。

表达式

一些特别的点:

  1. ~=相当于c语言里面的!=,不等于。
  2. table、userdata和function是引用比较,只有两个变量指向同一个对象才是相等。
  3. 逻辑运算符是"and or not",但是这里的and和or意思跟c语言有不同:
    1. a and b:如果a为false,则返回a,否则返回b;
    2. a or b   :如果a为true,则返回a,否则返回b。
  4. “..”两个点,表示字符连接符;如果操作数是number,则转换为字符串:
print(1 .. 2)   --> 12,注意..前后需要有空格,否则会报错

注意这里1 ..之间有一个空格,不然会报错。

但是如果是字符串就不需要:

print("hello".."world")	--> helloworld

表的构造

下面是基本例子:

table1 = {}                             --> 空表
names = {"jack", "john", "jimmy"}       --> 列表形式初始化
print(names[1])                         --> jack,下标从1开始
names[4] = "jason"                      --> 动态添加
print(names[4])                         --> jason
print(names[5])                         --> nil,因为不存在
a = {x = 0, y = 1}                      --> record形式初始化
print(a.x)                              --> 0
print(a["x"])                           --> 0,另一种表示方式
b = {"helloworld", x = 1, "goodbye"}    -->混合形式
print(b[1])                             --> helloworld
print(b.x)                              --> 1
print(b[2])                             --> goodbye
-- print(b.1)                           --> 没有这种

表中分隔可以使用逗号,也可以使用分号。

还有一种更一般的形式:

a = {["+"] = "add", ["-"] = "sub", ["*"] = "mul", ["/"] = "div", }  --> 后面可以有逗号
print(a["+"])   --> add
b = {[1] = "one", [2] = "two", [3] = "three"}
print(b[1])     --> one

控制语句

IF语句

if xxx then xxx end
if xxx then xxx else xxx end
if xxx then xxx elseif xxx then xxx else xxx end

elseif可以有很多个,注意else和if中间没有空格!!

WHILE语句

while xxx do xxx end

REPEAT-UNTIL语句

repeat xxx until xxx

FOR语句

for var=exp1,exp2,exp3 do xxx end

这里for里面的语句意思是var以exp3为step,从exp1到exp2。

需要注意几点:

  • 几个exp只会运行一次,且在for循环之前;
  • var是局部变量;
  • 循环过程中不要改变var。
for x,y,z in xxx do xxx end

BREAK和RETURN语句

break退出当前循环;return退出函数并返回结果。

注意break和return只能用在一个block的结尾。如果有时候确实想要在另外的地方使用,可以用这样的方式:

do break/return end

函数

函数的定义:

function funcName (args)
    states
end

关于函数调用时,如果没有参数,需要使用();

如果参数是字符串或者表构造时,可以不用()。

Lua函数中实参和形参的匹配也比较不讲究,实参多于形参,多余部分会忽略;实参少于形参,少的形参会是nil。这个与多变量赋值方式一致。

函数可以返回多个值:

function func (void)
    return 1,2
end

a,b = func()
print(a)    --> 1
print(b)    --> 2

通过()可以是函数强制返回一个值:

function func (void)
    return 1,2
end

print(func())       --> 1 2
print((func()))     --> 1

Lua可以有可变参数。使用...三个点表示,在函数中用一个叫arg的table表示参数,arg中还有一个域n表示参数的个数。

下面是一个例子:

function func_1(...)
    print("The number of args: " .. #{...})
    for i,v in ipairs({...}) do
        print(i,v)
    end
end
func_1(0, 1, 2, 3)

打印的结果是:

The number of args: 4
1       0
2       1
3       2
4       3

函数参数传递时可以使用表的形式,这样就不需要记太多的参数,如下面的例子:

--> 不需要特别创建函数func(new, old)
function func_2 (args)
    print(args.new,args.old)
end
var = {new = 1, old = 2}
func_2(var)

函数实际上也是变量,因此可以有另外一种表达方式,比如下面两种方式:

function foo(x) return 2 * x end
foo = function (x) return 2 * x end

它们其实是一样的。第二个foo被称为第一类值函数,将它应用在表中是Lua面向对象和包机制的关键。

函数中还可以包含函数,这个内部的函数可以访问包含它的那个函数中的变量(包括参数),下面是一个例子:

--> 定义了一个返回函数的函数
function NewCounter()
    local i = 0
    return function()
        i = i + 1
        return i
    end
end
c1 = NewCounter()   --> c1是被返回的函数,需要注意的是它捕获了NewCounter()中的i变量
print(c1())         --> 1
print(c1())         --> 2
c2 = NewCounter()
print(c2())         --> 1,c2是一个新的返回函数,它捕获的还是原来的i,所以打印的是1

这里的内部函数以及它能够访问到的(捕获的)变量,共同构成了称为"闭包(closure)"的概念。

典型的闭包包含两个部分,一个是闭包自己,另一个是工厂(创建闭包的函数)。

Lua中的函数是变量,因此它可以是全局变量或者局部变量。作为局部变量的例子,比如说表中的域(像io.read这种,read就是一个局部函数):

Lib = {}    --> 定义一个表
Lib.add = function(x, y) return x + y end   --> 创建两个局部变量
Lib.sub = function(x, y) return x - y end

print(Lib.add(1,1)) --> 2
print(Lib.sub(1,1)) --> 0

表中的局部函数还有两种形式可以表示:

第一种:

Lib1 = {
    add = function(x, y) return x + y end,  --> 分隔符别忘了
    sub = function(x, y) return x - y end
}   --> 定义一个表
print(Lib1.add(1,1)) --> 2
print(Lib1.sub(1,1)) --> 0

第二种:

Lib2 = {}
function Lib2.add(x, y) return x + y end
function Lib2.sub(x, y) return x - y end

print(Lib2.add(1,1)) --> 2
print(Lib2.sub(1,1)) --> 0

另外,在函数前面声明local就得到局部函数:

do
    local foo = function(x)
        print(x)
    end
    local function goo(x)
        print(x)
    end

    foo(1)
    goo(2)
end
-- foo(1)   --> 报错,访问不到了
-- goo(2)   --> 报错,访问不到了

声明局部函数的时候需要注意递归的情况,下面是一个错误的声明:

local func_3 = function (x)
    if x == 0 then
        return 1
    else
        return x * func_3(x - 1)
    end
end

print(func_3(3))

如果只在func_3的全面声明,函数内部的func_3(x - 1)位置就会识别不出来它是局部的,所以去找全局的了,结果没有找到就会报错,所以这里要做前向声明:

local func_3

func_3 = function (x)
    if x == 0 then
        return 1
    else
        return x * func_3(x - 1)
    end
end

print(func_3(3))

另外Lua中的函数还支持“正确的尾调用”,它像下面的样子:

function f(x)
  return g(x)
end

在这里g的调用就是尾调用,这种情况下,实际上g返回时不需要返回到调用者f中,所以不需要在栈中保留调用者f的任何信息。而正确的尾调用就是指Lua能够使尾调用时不使用额外的信息,这相当于一个goto到了另外的函数。

正确的尾调用使得递归可以无限进行而不会导致栈的溢出,当然也不只是在递归中,其它多个函数之间的调用也不需要担心栈溢出。

但是需要注意尾调用的形式,像下面这些就不是尾调用

function f()
  g()
  return
end
return g(x) + 1
return x or g(x)
return (g(x))

迭代器和泛型for

Lua中的迭代器常用函数来描述迭代器,每次调用该函数就返回集合的下一个元素。

下面是一个例子:

function IterFactory(t)     -->创建迭代器的工厂
    local i = 0
    local n = #t            -->获取表的长度,只对列表形式的有效
    return function()       -->这个是迭代器
        i = i + 1
        if i <= n then
            return t[i]
        end
    end
end

aTable = {"one", "two", "three"}
f = IterFactory(aTable)
print(f())  --> one
print(f())  --> two
print(f())  --> three
print(f())  --> 不打印

上面的例子其实跟《函数》一章中的闭包例子没有多大的差别。这个主要要讲的是下面这种和泛型for一起使用的情况:

aTable = {"one", "two", "three"}
for element in IterFactory(aTable) do
    print(element)
end

泛型for的格式是下面这样的:

for <var-list> in <exp-list> do
  <body>
end

其中,var-list的第一个变量称为控制变量,如果它是nil,循环终止;exp-list通常是一个迭代工厂的调用,就像前面的IterFactory(aTable)。

泛型for循环的执行过程如下:

  1. 计算in后面表达式的值,这个表达式返回三个值,分别是迭代函数,状态常量和控制变量,如果返回的不够,就用nil来补充;
  2. 将状态常量和控制变量作为参数调用迭代函数。(对于for循环,状态常量没有用,仅仅在初始化时获取它的值并传递给迭代函数);
  3. 将迭代函数返回的值赋值给变量列表;
  4. 如果返回的第一个值是nil,循环结束,否则执行循环体;
  5. 回到第二步再次调用迭代函数。

所以说,上面的泛型for可以解释称下面的伪代码:

do
    local _f, _s, _var = exp-list
    while true do
        local var_1, ..., var_n = _f(_s, _var)
        _var = var_1
        if _var == nil then
            break
        end
        <block>
    end
end

编译/运行/调试

首先介绍chunk的概念:

chunk是一系列的代码。Lua执行的每一个块代码,比如一个文件或者交互模式下(命令行下运行Lua不加参数,就会进入交互模式)的每一行都是一个chunk。

chunk可以是一个语句(比如:交互模式下的一行,do end内部只有一行的话也算),可以是一系列的语句(比如do end内部,或者for循环内部),还可以是函数。

下面介绍几个函数:

dofile():用来连接外部的chunk,它加载文件并执行它。下面是一个例子:

dofile("test_helloworld.lua")

这里的"test_helloworld.lua"是放置之前例子的文件,通过dofile()就可以运行它的代码。

dofile是Lua运行代码的一种原始操作,它只是辅助作用,真正完成功能的是loadfile()函数。

loadfile():它编译代码成中间码并且返回编译后的chunk作为函数,但不执行代码。

注意Lua语言也有编译这个动作,虽然它被称为脚本语言,但是Lua的编译器是语言运行时的一部分,所以执行编译并生成中间码的动作非常快。

另外,loadfile()不会抛出错误信息而是返回错误码,当发生错误时,loadfile()返回nil和错误信息。

与loadfile()很相似的有一个loadstring()。

loadstring():从一个字符串中读入代码并编译成函数。

f = loadstring("i = 1; print(i)")
f() --> 这里才是执行,前面只是生成了函数

或者简写成:

loadstring("i = 2; print(i)")() --> 注意后面的括号

loadstring()和loadfile()一样,不会抛出错误,但是会返回nil和错误信息,下面是一个例子:

print(loadstring("i i"))

它打印如下的错误信息:

nil     [string "i i"]:1: syntax error near 'i'

需要注意的一点,loadstring()总是在全局环境下编译字符串,所以它只认全局的变量而看不到局部变量:

local la = 0
loadstring("print(la)")()    --> nil

还有一个跟dofile()类似的函数是require():

require():它用来加载运行库。与dofile的主要区别是以下几点:

  1. require()会搜索目录加载文件。
  2. require()会判断是否文件已经加载以避免重复加载同一文件。

关于第一个区别:require()搜索的路径采用模式列表的形式,比如如下的形式:

?;?.lua;/usr/local/lua/?/?.lua

在实际的操作中,比如require("helloworld"),则参数代替模式中的?,并搜寻代替后的文件:

helloworld;helloworld.lua;/usr/local/lua/helloworld/helloworld.lua

下面是一个例子:

require("test_helloworld")

实际上它运行了helloworld.lua文件。

需要注意这里的路径保存在LUA_PATH这个全局变量中,但是它也不一定存在,如果不存在,就用默认的"?;?.lua"。

关于第二个区别:Lua中有一个表记录所有已经加载了的文件,但是需要注意表中保存的是文件的虚名,即参数名字。因此如果使用require("helloworld")和require("helloworld.lua"),那么实际上还是会加载两次helloworld.lua文件,而不是一次。

error():结束程序并返回错误信息。下面是一个例子:

error("Error happened!")

assert():接受两个参数,第一个参数如果不为真,就调用error(第二个参数表示的信息)。

pcall():封装可能的错误代码(错误代码被放在函数中,或者包装成一个匿名函数),用来进行错误处理。如果没有错误,返回true和调用函数的返回值,否则返回false加错误信息。由于error()函数会直接返回,所以使用pcall()的话,至少可以保证让代码继续运行下去。

下面是一个例子:

--> 可能出错的代码封装在函数内
function func()
    error() -->抛出错误
end
--> pcall调用函数
if pcall(func) then
    print("no error")
else
    print("error")
end

上述的代码打印error。

不仅是错误信息,当发生错误的时候,所有传递给error()的参数都会被pcall()返回:

--> 可能出错的代码封装在函数内
function func()
    error({code = 404}) -->抛出错误
end
--> pcall调用函数
local status, err = pcall(func)
if status == false then
    print(err.code)
end

打印结果:404。

协程coroutine

协程跟线程类似,不同的是同一时间可以跑多个线程,但是协程只能有一个在运行。

协程通过协作来完成,Lua里面就是resume()和yeild()。

协程需要运行的代码被封装在函数中,通过将函数名传递给create()函数作为参数来创建一个协程,当然也可以直接传递匿名函数当作参数。

下面是一个例子:

co = coroutine.create(
        function ()
            print("helloworld")
        end
    )

print(co)   --> thread: 0xxxxx(某个16位地址)

协程有三个状态:挂起态,运行态,停止态。

协程被创建后处于挂起态。也就是说协程并不会自动运行。

通过协程的status()来查看状态:

co = coroutine.create(
        function ()
            print("helloworld")
        end
    )

print(co)                   -->thread: 0xxxxx(某个16位地址)
print(coroutine.status(co)) -->suspended

通过resume()可以使协程进入运行态,之后代码结束就进入了停止态:

co = coroutine.create(
        function ()
            print("helloworld")
        end
    )

print(co)                   -->thread: 0xxxxx(某个16位地址)
coroutine.resume(co)        -->running,打印helloworld
print(coroutine.status(co)) -->dead

通过yield()可以将协程挂起:

co = coroutine.create(
    function ()
        for i=1,10 do
            print("co", i)
            coroutine.yield()
        end
    end
    )

coroutine.resume(co)
print(coroutine.status(co)) -->suspended

运行结果如下:

co      1
suspended

如果之后再运行resume():

co = coroutine.create(
    function ()
        for i=1,10 do
            print("co", i)
            coroutine.yield()
        end
    end
    )

coroutine.resume(co)
print(coroutine.status(co)) -->suspended
coroutine.resume(co)
coroutine.resume(co)
coroutine.resume(co)
coroutine.resume(co)

运行结果如下:

co      1
suspended
co      2
co      3
co      4
co      5

当调用次数超过10次之后:

print(coroutine.resume(co))

运行结果如下:

false   cannot resume dead coroutine

resume()和yield()之间可以传递参数,见下面的示例:

示例一,resume()的参数传递给了协程:

co = coroutine.create(
    function (a, b, c)
        print("co", a, b, c)
    end
    )

coroutine.resume(co, 1, 2, 3)   --> resume的参数传递到了协程中

运行结果如下:

co      1       2       3

示例二,yield()的参数也将传递给resume():

co = coroutine.create(
    function(a, b)
        coroutine.yield(a + b, a - b)
    end
    )

print(coroutine.resume(co, 20, 10))

运行结果如下:

true    30      10

运行结果中,true表示resume()运行成功,后面的30和10是yield的参数。

示例三,协程代码返回值也会传递给resume():

co = coroutine.create(
    function(a, b)
        return a + b, a - b
    end
    )

print(coroutine.resume(co, 20, 10))

运行结果跟之前那个一样。

协程是非抢占式的,因此如果一个线程阻塞了,整个程序也就停止了。(当然这个也是有办法解决的,不过在基础部分不讲)

  • 11
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值