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、操作符优先级
其中“^”和”..”是“右结合”的,即自右向左结合,其余为左结合。 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=1}, {x=0, } --“;”常用来分割两种不同风格,用“,”也没错 其中{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”,”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,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,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,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",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 函数名(参数列表)??? <function body> 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,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,3}))????????? -->1,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,3) 普通指定参数个数的函数也可以用这种形式来表示: function foo(a,c)等同于 function foo(…) ?????? local a,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)。所以函数与string、number一样都是匿名的,都是一个值,而函数名如普通变量名一样只是一个指向这个值“引用”名称。例如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) <body> 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(<args>) <body>??????????????? ?????????????????????????????????????????????????????????????????????????????????????????????② end 在定义递归函数时,采用第一个方式就不可行,而第二种却可以。因为“语法糖”的展开实际上是 local f f=functionf(<args>) ?????? f(<args >) --递归调用f。及在函数定义时,这个局部变量的值尚未完成定义,但之后的函数执行时,f已经拥有的了正确的值。 end 区别于 local f=functionf(<args>) ???? f(<args>) --递归调用f,会出错!因为f本身没有定义完毕,这里调用f会被认为是一个全局的f而非当前要定义的f end 再比较以下两段程序:
? ?? 左边的程序会出错,因为test2未定义。而右边程序可以,因为在运行时test2已经有了正确内容。 但对于间接递归(f中调用g,g中调用f)②也不可用了。必须是③的明确的前向声明。 3、尾调用尾调用是指一个函数调用是另一个函数最后一个动作。只有“return<func>(<args>)”的形式是一条尾调用。并不意味着最后一句话是一个函数调用就是一个尾调用,而是最后一个动作: 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)的动作。 尾调用不会耗费空间,故一个程序尾可以调用无数嵌套的尾调用,并且不会导致栈溢出。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |