Lua学习笔记(二)

四、表达式

1、算数操作符

      除了基本的“+ - * /”,Lua中特殊的在于“^”表示指数运算;“%”取模可用于任何实数(而非C中仅限整型)。

a=8^(1/3) --计算8的1/3次

10.3%4    --等于2.3

其中“%”是根据以下规则定义的:

a%b=a-floor(a/b)*b      --floor是向下取整函数

      故对于小数取模有一些特别的用法:

x%1结果是x的小数部分,x-x%1就是x的整数部分。x%0.01是x小数点后两位之后(小数点后第三位开始)的部分,x-x%0.01就是x精确到小数后两位的结果。类似地,可以x-x%0.001来获得精确到小数点后3位的结果。

2、关系操作符

“<”、“>”、“<=”、“>=”、“==”、“~=”

其中“~=”表示不等于。如果两个值的类型不同,则认为不等。关系操作符运算结果返回true或false。

值得注意的是只有同种类型的值才能进行大小性判断,不同类型的值只能进行相等性判断

2<”15”  --类型不同,出错

2 == “15”--可以判断

对于table、userdata、function,Lua是做引用比较的。只有当它们引用用一个对象时,才认为相等。并且只能进行相等性判断。

只有数字和字符串可以进行大小性判断。

3、逻辑操作符

     and      or        not

区别于C和Java的是,逻辑操作符返回的结果不一定是boolean型的,也可能是一个操作数

与条件控制语句一样,所有的逻辑操作符将false和nil作为假,其他任何视为真。

对于and,如果第一个操作数为真,返回第二个操作数;如果第一个操作数为假,返回第一个操作数——操作数本身的值,故操作数是什么就返回什么。

print(4 and 5) -->5

print(false and 5)   -->false

print(nil and 3)       -->nil

同理,对于or也是返回操作数。第一个操作数为真,则返回第一个操作数,否则返回第二个操作数。

print(4 or 5)           -->4

print(false or 5)      -->5

print(nil or false)    -->false

与C相同的是,Lua的逻辑操作符也会“short-cut evaluation”,即前面的条件已可得到结果就不计算后面的。

Lua中常用“x=x or v”来对x设默认值为v。这句话表示x为假(没有设置x时为nil),则取v的值;如果不为假则不改变。

还有“a and b or c”,当b恒为真时,等价与C中的“a?b:c”。常用于求两者之间的最大值:

max=(x>y) and x or y

x>y时,(x>y)为真,(x>y) and x等于x,x必为真故x or y等于x,所以max=x;反之(x>y)为假,(x>y) and x返回假,假or y等于y。(and的优先级高于or)

非操作符not只返回true或false。

 

4、操作符优先级

 

 

 

 

 

 

^

not   #  - (一元操作符)

*   /  %

+  -

..

<      >      <=      >=       ~=      ==

and

or

其中“^”和”..”是“右结合”的,即自右向左结合,其余为左结合。

5、构造式

构造式是用于初始化table的。除了用{}来初始化一个空table外,还有“列表”风格、“记录”风格、“通用”风格的方式来初始化table。

(1)“列表”风格

days={“Mon”,”Tue”,”Wed”,”Thu”}

这种方式直接对table赋值,不指定索引值,故默认为是一个普通数组,默认索引为从1开始的整数。故days[1]=”Mon”;days[2]= ”Tue”。

(2)“记录”风格

      “记录”风格即“什么等于什么”的形式,指定索引值和元素值。

      a={x=1,y=2}  -- 等价于a={};a.x=1;a.y=2

      x=1,y=2中x、y是字符串“x”、“y”。而不是变量x(如果前面有定义x变量)

      可以用这种方式实现链表:

      list=nil

       for i=1,10 do

              list={next=list,val=i}

       end

      上述程序得到一个反序的链表。链表的每一个结点都是个table,table中包含next和val两个字段。next指向上一次创建的表,所以是反序的:

      nilß1ß2ß3ß4ß5ß6ß7ß8ß9ß10

      “记录”风格指定了索引(字段名),就可以用“表名.索引名”的方式引用元素。如print(a.x)

     (3)“列表”风格与“记录”风格结合

polyline={color=”red”,thickness=2,npoint=4;

{x=0,y=0},

{x=-10,y=0},

{x=-10,y=1},

{x=0,y=1},

} --“;”常用来分割两种不同风格,用“,”也没错

其中{x=0,y=0}是一个table,作为polyline的元素,由于其形式是“列表”风格,故其索引值为1.有

print(polyline[2].x)  -->-10

print(polyline[4].y)  -->1

(4)通用”风格

“列表”风格与“记录”风格都存在不能使用负数、运算符作为索引的限制。Lua提供了通用的初始化格式——在方括号之间,显式地用一个表达式来初始化索引值:

opsnames={[“+”]=”add”,[“-”]=”sub”,[“*”]=”mul”,[“/”]=”div”}

print(opsnames["+"])    -->add

i=20;s=”-”;

a={[i+0]=20,[i+1]=s..s}      -->a[20]=20;a[21]=”--”

之所以是通用的,“列表”风格与“记录”风格可以用这个方式表示:

{x=1,y=2}      --等价于{[“x”]=1,[“y”]=2}

{“r”,”g”,”b”}    --等价于{[1]=”r”,[2]=”g”,[3]=”b”}

还可以指定数组从0开始:

days={[0]=”Sun”,“Mon”,”Tue”,”Wed”,”Thu”}则days[1]= ,“Mon”。但不推荐将Lua的数组从0开始。

 

五、语句

1、赋值

      Lua支持“多重赋值”,即用一个赋值符号(“=”)对多个变量赋值,每个变量之间用“,”分隔:

      a,b=1,2          -- a=1;b=2

      当变量数量大于值的数量时,多余的变量赋nil值:

      a,b,c=0          --a=0;b=nil;c=nil

      当值的数量大于变量数量时,多余的值被舍弃:

      a,b=1,2,3       --a=1;b=2;3被舍弃

      Lua对于多重赋值,是先将“=”后面的值从左到右计算之后寄存,然后对变量依次赋值。所以多重赋值可用于值的交换

      x,y=y,x           --x,y互换

一般情况很少会对一组没有关联的值一起赋值。多重赋值也没有比分开赋值高效。多重赋值通常用于上述的两值交换和收集函数(如string.find)的多个返回值。

2、局部变量

      Lua中用local关键字类声明局部变量。局部变量的作用域仅限于它所在的块(block)。一个块是指一个控制结构(如if..then..else、while等)的执行体、或者是一个函数的执行体(function..end)或是一个程序块。

      x=10              --全局变量x

local i=1  --局部变量i

while i<=x do --此处的x是全局的x(等于10)

              print(x)

              local x=i*2             --局部变量x,在其作用域内覆盖全局的x

              print(x)

              i=i+1

end

上述程序应放在lua文件中执行,如果在交互模式下,没行输入内容就形成一个程序块。当输入local i=1是就会立马执行。而下面的语句是另一个独立的程序块,i的作用域不能达到。若要在交互模式下执行,应在最外层加上do-end(显式的界定一个块)。当输入do时,Lua就不会单独执行后面每行的内容,而是知道遇到一个相应的end才执行整个块的内容。

      使用局部变量的优势:

      ①避免将一些无用的名称引用全局环境,破坏全局环境

②访问局部变量比访问全局变量更快

③局部变量随着作用域结束而消失,利于垃圾收集器回收释放

3、控制结构

Lua控制语句都有显式的终止符:if、for、while以end为结尾,repeat以until为结尾。

(1)if …then… else…end

if或者elseif后面都要加then来跟要执行的语句,else之后不需要then:

if(表达式1) then

       执行语句1

elseif(表达式2) then

       执行语句2

elseif(表达式3) then

       执行语句3

else

执行语句4

end

if..then..end可单独使用,不支持switch语句。

(2)while…do…end

while后要加do来跟执行语句:

while 条件表达式 do

       执行语句

end

(3)repeat…until

相当于C中的do…while语句。测试语句在循环体之后,所以至少会执行一次循环体:

repeat

       循环体

until 条件表达式

值得注意的是,在Lua中一个声明在循环体重的局部变量的作用域包括了until中的测试条件

y=10

repeat

       local i=y/2

       print(i)

       y=y-1

until i<=0              --在此仍可访问i

(4)数字型for(numeric)

for语句有两种:数字型for和泛型for

数字型for语法如下:

for var=exp1,exp2,exp3 do

       <执行体>

end

var从exp1变到exp2,exp3是步长,省略时默认为1,exp3可以是负数。

值得注意的是,var变化范围可以是任意实数(小数),但由于Lua用双精度表示任意数字,会存在误差,导致循环范围与定义的不同的情况:

for i=1,2,0.1 do

       print(i)

end

输出1,1.1,1.2,1.3,1.4,1.5,1.6,1.7,1.8,1.9,而2没有输出!改成

for i=1,2.000000000000001,0.1 do

       print(i)

end

则输出2。验证程序:

print(2<2.0000000000000001)     -->false

print(2==2.0000000000000001) -->true

另外Lua只能计算到小数点后13位:

print(1.9000000000001+0.1000000000001)    --> 2.0000000000002,多一个0就变成2了。

数字型for需要注意的点:

①for的3个表达式是在循环开始前一次性求值的,即exp1,exp2,exp3只会执行一次!所以结束条件、步长不能是动态的!

②var被自动声明为for语句的局部变量,尽在循环体中可见。

③不要在循环体中修改控制变量var,否则会导致不可预知的效果。

若不想给循环设置上限的话,可以使用常量math.huge(a value larger than or equal to any other numerical value):

for i=1,math.hugedo …end

(5)泛型for(generic for)

泛型for通过一个迭代器遍历table。标准库提供的迭代器有:

io.lines()                --用于迭代文件中的每一行

pairs()             --用于迭代table所有的元素

ipairs()             --用于迭代数组的元素

string.gmatch()  --迭代字符串中单词

泛型for要配合关键字in使用,以下是pairs和ipairs的区别:

a={[1]="111",[2]="222",[5]="555",x=1,y=2,z=3}

for k,v in pairs(a) do

       print(k,v)

end

--输出a中所有的key-value,顺序不一定是定义时的顺序

for k,v in ipairs(a) do

       print(k,v)

end

--只输出[1]="111",[2]="222"

分析:pairs返回的迭代器包含table中所有的key-value对;

ipairs只返回数组元素,即下标为从1开始的整数做key的key-value对。当遇到table[i]=nil时就终止。所以[5]="555"没有输出,因为a[3]=nil。

如果一个table中table[1]没有定义,则用ipairs返回的迭代器没有元素。如把a改成a={[2]="222",[5]="555",x=1,y=2,z=3}则ipairs(a)不能输出任何值。

也可以只给key赋值:

for k in pairs(a) do print(k) end

(5)break和return

break和return的用法与C中相同。break和return只能用在block结束的地方, 不能用在block的一个中间部分,即后面跟的是end或者elseif、esle

function test()

       return --doreturn end

       print("abc")

end

上述程序会输出abc,但如果在return前后加上do-end就可直接返回,而不输出。

 

 


六、函数

function 函数名(参数列表)    end

当调用的函数参数只有一个,并且此参数是一个字符串常量或table构造式时,则可以不加圆括号,如:

print “hello”           -- print (“hello”)

func{x=10,y=20} --func({x=10,y=20}).{x=10,y=20}是一个table构造式

给函数传入参数时,实参数量可以与形参数量不同。这与多重赋值一样“多舍弃,少nil”的方式处理。这种机制有一些作用。如下面的程序:

count=0

function iCount(n)   --全局计数函数

       n=n or 1

       count = count + 1

end

当调用iCount()(即不传入参数)时,n被初始化为nil,在函数中又被初始化为1,起到了对全局变量count加1的用作。

当一个lua文件中定义相同名称(参数列表不同)的时候,后面定义函数会覆盖前面的。

1、多重返回值

Lua支持函数返回多个值,在return后加上要返回的值,并用逗号分隔。

多重返回值的函数调用在不同情况下会得到不同的结果:

(1)作为单独的语句时(不将结果赋给任何变量),函数返回值会被舍弃;

(2)函数作为表达式的一部分时(如算数表达式),只保留第一个返回值;

(3)只有以下4种情况中,函数调用是最后一个元素时才能获得所有返回值:

假设foo()有返回”a”,”b”。

①多重赋值

x,y,z=”c”,foo()              --x=”c”;y=”a”;z=”b”

x,y,z=foo(),”c”              --x=”a”;y=”c”;z=nil;因为foo()不是赋值表达式的最后一个元素!

②一个函数调用作为另一个函数的参数列表

最常见的就是print函数,该函数支持任意多个参数。函数调用作为要输出的内容时,放在最后才能输出全部返回值——

print(foo(),1)         -->a,1

print(1,foo())         -->1,a,b

对于一般的多个形参的函数,其传入的实参是函数调用时也要注意是否在最后位置,否则也当做一个参数传入,若实参个数小于形参个数就会初始化为nil了!

③用table构造式中调用函数

t={foo(),1}    --t[1]=”a”;t[2]=1

t={1,foo()}    -- t[1]=1;t[2]=”a”; t[3]=”b”;

④return一个函数调用

return 1,foo() --返回1,“a”,“b”

return foo(),1 --返回“a”,1

注意以上4中情况都是在函数调用作为最后一个元素时有效

可以将函数调用放入一对圆括号中,迫使它只返回一个结果:

print((foo()))          -->a

关于多重返回值,有一个特殊函数——unpack。该函数接受一个数组作为参数,返回从下标1开始的所有元素,直到遇到nil元素。

print(unpack({1,2,3}))          -->1,2,3

unpack函数是泛型调用的体现,即unpack的参数可以是任何类型的数据。

2、变长参数

Lua支持函数的参数个数是不定的,用“…”来表示形参。如以下程序:

function add(...)

       local sum=0

       for k,v in ipairs{...} do    --{...}表示以“...”组成的数组

              sum=sum+v

       end

       return sum

end

函数中访问它的变长参数也要用到“…”,其中{...}表示由“...”组成的数组。在调用add函数时直接传入所有参数:

add(1,2,3)

普通指定参数个数的函数也可以用这种形式来表示:

function foo(a,b,c)等同于

function foo(…)

       local a,b,c = …

end

这种用“…”表示参数的方式对跟踪某个特定的函数调用有帮助。

具有变长参数的函数同样可以有任意数量的固定参数,但固定参数要在“…”之前。如

io.wirte(string.format(fmt,…))

fmt是固定参数(指定的字符串格式),“…”是变长参数。

{...}用于遍历数组,但有时候变长参数在传入实参时会特意传入nil值,这样{...}不能访问所有的参数,此时用到select。

select函数的第一个参数是指定返回第几个参数,第二个参数是“…”。

select(n,…)            --返回“…”中的第n个参数

select(“#”,…)         --返回“…”中所有参数的总个数

functionvariable_arguments(...)

       for i=1,select("#",...) do -- select("#",...)指定循环上限

              local arg=select(i,...)   -- select(i,...)选出第i个参数,可以是nil

              print(arg)

       end

end

Lua5.0对于变长参数是采用一个隐含的局部table变量“arg”来接收所有变长参数的,arg有一个“n”字段来记录arg的长度。但这种机制 缺点在于每当程序调用一个具有变长参数的函数时都会创建一个table。(概述中提供arg是存放脚本启动参数的全局变量)

3、具名实参

传入函数的参数根据其实参的顺序与形参匹配,但有时候也可以通过名称来指定实参。这时需要用一个table作为函数参数,在传入实参时指定参数是table的哪一个字段。这种方式对于有大量参数并且大部分是可选参数的函数有帮助(对于未指定的字段有默认值,只需传一些必要参数即可)。

function rename(t) --定义rename函数,参数t是一个table

       return os.rename(t.old,t.new) --os.rename是os库的重命名函数

end

rename{old=”old.lua”,new=”new.lua”} --调用rename,指定字段的值

 

 

七、深入函数

在第三章中就提到Lua的变量类型包括了function,所以Lua中函数与其他传统数据类型的统一级别的——“第一类值”(First-Class Value)。所以函数与stringnumber一样都是匿名的,都是一个值,而函数名如普通变量名一样只是一个指向这个值“引用”名称。例如print函数只是一个具有打印功能的变量,“print”这个名字可以指向别的函数:

a={p=print}           --a是一个table,字段p指向print函数

a.p(“hello”)            --打印hello

print=math.sin       --变量名“print”指向正弦函数

a.p(print(1))          --打印sin(1),0.841470

sin=a.p                 --变量名“sin”指向了原来的打印函数

sin(10,20)             -->10      20

因此函数的本质是“值”,函数名是指向它的变量名,函数的本质定义是一种赋值语句,这条语句创建了一个类型为“函数”的值

foo = function (x)return 2*x end

它等同与functionfoo(x) return 2*x end,而这种传统的定义方式是一种“语法糖”而已。

可以将表达式“function(x) end”视为一种函数的构造式,就像table的{}一样。将这种函数构造式的结果称为“匿名函数”。

一般情况下为了方便使用函数都会给函数一个名字,但在某些情况下使用匿名函数带来方便——将函数作为参数传入另一个函数。这里的函数作为参数是指参数类型就是函数,而不是一个函数调用(函数的返回值)。这类似于C中的函数指针和Java的匿名内部类。table.sort就是一个接受另一个函数作为参数的排序函数。它的第一个参数是要排序的table名,第二参数是一个函数(“次序函数”)。这个次序函数负责实现具体的排序方式(升序、降序、还是按哪种关键字顺序,类似Java的Comparator)

像sort这样以另一个函数作为参数的函数称为“高阶函数”,其实质仍是一个值。

-- derivative是一个近似求导函数,以函数作为参数,返回值也是一个函数

functionderivative(f,delta)

       delta=delta or 1e-4

       return   function (x)

                            return(f(x+delta)-f(x))/delta

                     end

end

c=derivative(math.sin)

print(math.cos(10),c(10))             -->-0.83907152907645   -0.83904432662041

1、闭包函数(closure)

Lua允许在一个函数中定义一个新的内部函数,并且这个新的内部函数可以访问外部函数的局部变量,这个特征称为“词法域”。“词法域”与“第一类”特性使Lua编程变得简洁、灵活多变。

在内部函数中可以调用属于外部函数的局部变量,而这个局部变量在内部函数中既不是局部变量也不是全局变量,而是“非局部的变量”(non-local value/upvalue)。

function newCounter()    --计数函数,该函数返回值是一个函数

       locali=0

       returnfunction ()    --匿名的内部函数调用外部函数的局部变量

                     i=i+1

                     returni

                 end

end

c1=newCounter()   --c1获得一个返回的函数

print(c1())                    -->1

print(c1())                    -->2

c2=newCounter()   --c2获得一个返回的函数

print(c2())                    -->1

print(c2())                    -->2

上述程序中c1虽然获得了一个内部函数,但看上去已经超出了i的作用域,所以在i=i+1时应该不能正常使用。但Lua有一个“闭包”(closure)来处理这种情况。所谓closure就是一个函数及一系列这个函数会访问到的“非局部的变量”。所以i仍可以使用。从c2可以看出,c1、c2分别持有各自的closure。

因此,函数本身就是一种特殊的closure——即没有“非局部变量”的closure。

closure的作用:

①作为table.sort这种高阶函数的参数;

②用于那些创建其他函数的函数,如上面的newCounter()。又比如在GUI设计中会很实用;

③重新定义某些函数,甚至是重新定义那些预定义的函数(库函数):

do   --重写math.sin函数,将参数从弧度转为角度

       local oldSin=math.sin

       local k=math.pi/180

       math.sin=function (x) return oldSin(x*k)end

end

上面的程序把原来的sin函数保存在一个局部变量oldSin中(do-end界定了局部变量的作用域),do-end之外的函数用math.sin来计算时参数就变成了角度,并且不能掉用原来的sin函数(oldSin),做到了“彻底地”改变。

类似的还可以用于创建一个安全的运行环境。比如服务器要运行来自网络的程序,要检测这段程序对文件的访问权限,就可以重写io.open函数,在其中添加权限检查,避免非法访问。

2、非全局函数(non-globalfunction)

函数是值,所以可以定义在table的字段和局部变量中。

(1)函数定义在table中

如同io.read是在名为io的table中调用read字段(实为函数)一样,可以自定义带函数的table。只需将函数定义与table构造式结合:

①使用常规的函数语法与table语法相结合来创建局部函数

Lib={}

Lib. add=function(x,y) return x+y end

Lib. sub=function(x,y) return x-y end

②使用table构造式来创建局部函数

Lib={

       add=function (x,y) return x+y end,

       sub=function (x,y) return x-y end

}

③另类的方法创建局部函数

Lib = {}

function Lib.foo(x,y)

    return x + y

end

function Lib.goo(x,y)

    return x - y

end

(2)函数赋给局部变量

将函数存储到一个局部变量中,就得到了一个局部函数。局部函数的作用域限定在某个特定范围内。

local f1=function(<参数>)

   <函数体>                                                                                           ①

end

local g1=function(<参数>)

   <函数体>                                                                                          

    f1()   --f1在这里是可用的

end

或者用“语法糖”:

local functionf()

                                                                                                             ②

end

在定义递归函数时,采用第一个方式就不可行,而第二种却可以。因为“语法糖”的展开实际上是

local f

f=functionf()

       f() --递归调用f。及在函数定义时,这个局部变量的值尚未完成定义,但之后的函数执行时,f已经拥有的了正确的值。

end

区别于

local f=functionf()

     f() --递归调用f,会出错!因为f本身没有定义完毕,这里调用f会被认为是一个全局的f而非当前要定义的f

end

再比较以下两段程序:

 

local test1

local test2

test1=function ()

       print(1)

       test2()

end

test2=function ()

       print(2)

end

test1()                 ③

 

test1()

local test1=function ()

       print(1)

       test2()

end

local test2=function ()

       print(2)

end

test1()

    

左边的程序会出错,因为test2未定义。而右边程序可以,因为在运行时test2已经有了正确内容。

但对于间接递归(f中调用g,g中调用f)②也不可用了。必须是③的明确的前向声明。

3、尾调用

尾调用是指一个函数调用是另一个函数最后一个动作。只有“return()”的形式是一条尾调用。并不意味着最后一句话是一个函数调用就是一个尾调用,而是最后一个动作:

function   f(x) return g(x) end        --尾调用

function f(x) g(x)end                          --舍弃g(x)的返回值,所以不是一个尾调用

function f(x)return 1+g(x) end            --进行加法运算,所以不是一个尾调用

function f(x)return x or g(x) end   --必须将返回值调整为一个

function f(x)return (g(x)) end              ---必须将返回值调整为一个(g(x)可能有多个函数值),不是一个尾调用。

◆尾调用的作用

若g(x)是f(x)的尾调用,当g(x)执行完后就不需要返回f(x)了。因此程序不需要保存任何关于f(x)的栈信息——区别于一般的函数调用。一般的函数调用要保存f(x)的栈信息,待g(x)执行完后返回到f(x)继续执行,而尾调用则省去了返回f(x)的动作。

尾调用不会耗费空间,故一个程序尾可以调用无数嵌套的尾调用,并且不会导致栈溢出。

你可能感兴趣的:(lua)