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语言有不同:

a and b:如果a为false,则返回a,否则返回b;

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。

需要注意几点:

1. 几个exp只会运行一次,且在for循环之前;

2. var是局部变量;

3. 循环过程中不要改变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  in  do
  
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
        
    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))

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

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

 

你可能感兴趣的:(脚本)