Lua知识树整理————语言基础

Lua知识树整理

语言基础

    • lua语言中的标识符可以以各种下划线、字母、数字主程,但不能以数字开头

    • “下画线 大写字母”( VERSION )组成 标识符通常被 语言用作特殊用途,应避免将其用 他用 我通常会将“下画线 +小写字母”用作哑变量'( Dummy variable 以下是 Lua语言的保留字( reserved word ),它们不能被用作标识符

      and break do else elseif
      end false goto for function
      if in local nil not
      or repeat return then true
      until white

      注:lua语言是对大小写敏感的, 虽然 and 是保留字,但是 And AND 就是两个不同的识符

    • 注释

      • -- 和 --[[ ]]
      • [=[]=] 等号不定,按等号匹配
    • 语言中有 种基本类型: il (空) boolean (布尔) number (数值)、 string (字符串)、 userdata 户数据)、 funct on (函数)、 thread (线程)和 table (表) 使用函数 type可获取一个值对应的类型名称

    • 条件测试(例如控制结构中的分支语句)将除 Boolean false ni 外的所有其他值视为真,特别的是,在条件检测中 Lua 语言把 和空字符串也都视为真

    • Lua的and,or都是短路形式

    • 逻辑运算符and 的运算结果为:如果它的第一个操作数为“fal四”,则返回第一个操作数,否则返回第 个操作数

    • 逻辑运算or 的运算结果为:如果它的第一个操作数不为“false ”,则返回第一个操作数,否则返回第二个操作数

    • 有用的表达式形如(( and b) or )或( and b or c ) (由于 and 的运算符优先级高于 or ,所以这两种表达形式等价,后面会详细介绍),当 不为 false 时,它们还等价于其他语言的三目运算符 a?b:c

    • Lua 针对整数除法引入了一个称为 除法的新算术运算符:// 顾名思义, floor 除法会对得到的商向负无穷取整,从而保证结果是一个整数。这样, floor 除法就可以与其他算术运算一样遵循同样的规则: 如果操作数都是整型值,那么结果就是整型值,否则就是浮点型值(其值是一个整数)

    • a%b = a - ((a // b) * b)

    • 小技巧对于实数而言,取模有个技巧

      x = 3.1415926

      x - x%0.01 -->3.14

    • math.random

      • 系统math.random默认的随机种子是1,
      • 如果要改变可以修改randomseed的值。math.randomseed(os.time())
      • math.random()不给参数范围是[0,1)
      • math.random(n)给了范围是[0,n]
    • 可以通过+0.0或者-0.0快速的将整形转换为浮点型

    • 与0按位运算或可以将浮点型转换为整形(前提是小数部分都是0),也可以通过math.tointeger函数来转换,成功返回值失败返回nil

    • 可以使用长度操作符#获取字符串长度

      local x = "abc"
      print(#x)-->3
      x = "你好"-->6
      print(#x)
      

      特殊的,这个#其实是返回字节数,在某些编码中,这个值可能与字符串中字符的个数不同。

    • 我们可以使用连接操作符..来进行字符串连接。如果操作数中存在数值,那么 Lua 语言会先把数值转换成字符串

      local x = "hello"
      print(x.." word")
      print(x)
      
      --[[
      hello word
      hello
      ]]
      

      应该注意,在 Lua 语言中,字符串是不可变量。字符串连接总是创建一个新的字符串,而不会改变原来作为操作数的字符串

    • 我们可以使用一对双引号或单引号来声明字符串常量,它们两者唯一的区别在于,使用双引号声明的字符串中出现单引号时,单引号不需要转义,反过来依然成立。

    • 转义字符

      转义字符 含义
      \a 响铃 (bell)
      \b 退格(back space)
      \f 换页(form feed)
      \n 换行(newline)
      \r 回车(carriage return)
      \t 水平制表符(horizontal tab)
      \v 垂直制表符(vertical tab)
      \ 反斜杠(backslash)
      " 双引号(double quote)
      ' 单引号(single quote)
      print("one line\nnext line\n\"in quotes\", 'in quotes'")
      
      --[[
      one line
      next line
      "in quotes", 'in quotes'
      ]]
      

      在字符串中,还可以通过转义序列\ddd\xhh来声明字符。其中,ddd是由最多 3 个十进制数字组成的序列,hh是由两个且必须是两个十六进制数字组成的序列,从 Lua 5.3 开始,也可以使用转义序列\u{h...h}来声明 UTF-8 字符,花括号中可以支持任意有效的十六进制:

      print("\u{3b1} \u{3b2} \u{3b3}")
      --α β γ
      
    • 长字符串/多行字符串

      像长注释/多行注释一样,可以使用一对双方括号来声明长字符串/多行字符串常量。被方括号括起来的内容可以包括很多行,并且内容中的转义序列不会被转义。此外,如果多行字符串中的第一个字符是换行符,那么这个换行符会被忽略。多行字符串在声明包含大段代码的字符串时非常方便,例如:

      page = [[
           <html>
           <head>
               <title>An HTML Page</title>
           </head>
           <body>
               <a href="http://www.lua.org">Lua</a>
           </body>
           </html>
       ]]
       
       print(page)
      --[[
           <html>
           <head>
               <title>An HTML Page</title>
           </head>
           <body>
               <a href="http://www.lua.org">Lua</a>
           </body>
           </html>
      ]]
      

      有时字符串中可能出现类似 a = b[c[i]] 这样的内容(注意最后的]]),或者,字符串中可能有被注释掉的代码。为了应对这种状况,可以在两个左方括号之间加上任意数量的等号,例如:[=[。这样,字符串只有在遇到了包含相同数量等号的两个右方括号时才会结束(即]=])。Lua 语言的语法扫描器会忽略所含等号数量不同的方括号。通过选择恰当数量的等号,就可以在无须修改原字符串的情况下声明任意的字符串常量了。
      对于注释而言,这种机制同样有效。例如我们可以使用--[[和--]]来进行长注释,从而降低了对内部已经包含注释的代码进行注释的难度。
      当代码中需要使用常量文本时,使用长字符串是一种理想的选择。但是,对于非文本的常量我们不应一样的该滥用长字符串。虽然 Lua 语言中的字符串常量可以包含任意字节,但是滥用这个特性并不明智(例如,可能导致某些文本编辑器出现异常)。同时,像 "\r\n" 一样的 EOF 序列在被读取的时候可能会被归一化成 "\n" 。作为替代方案,最好就是把这些可能引起歧义的二进制数据用十进制数值或十六进制的数值转义序列进行表示,例如 "\x13\x01\xA1\xBB"。不过,由于这种转义表示形成的字符串往往很长,所以对于长字符串来说仍可能是个问题。针对这种情况,从 Lua 5.2 开始引入了转义序列 \z,该转义符的使用方法:

      data = "1234567\z
                89101112131415"
      print(data)
      --123456789101112131415
      
    • 强制类型转换

      • Lua 语言在运行时提供了数值与字符串之间的自动转换(conversion)。针对字符串的所有算术操作都会尝试将字符串转换为数值。Lua 语言不仅仅在算术操作时进行这种强制类型转换(coercion),还会在任何需要数值的情况下进行,例如函数math.sin()的参数。
        相反,当 Lua 语言发现在需要字符串的地方出现了数值时,它就会把数值转换为字符串。

      • 如果显示地将一个字符串转换成数值那么可以使用函数tonumber()。当这个字符串的内容不能表示为有效数字时该函数返回 nil;否则,该函数就按照 Lua 语法扫描器的规则返回对应的整型值或浮点类型值,默认情况下,函数tonumber()使用的是十进制,但是也可以指明使用二进制到三十六进制之间的任意进制

        print(tonumber(" 100101 ", 2))
        print(tonumber(" fff ", 16))
        print(tonumber(" -ZZ ", 36))
        print(tonumber(" 987 ", 8))
        --[[
        37
        4095
        -1295
        nil
        ]]
        
    • 字符串标准库

      • 为一种典型的应用,我们可以使用如下代码在忽略大小写差异的原则下比较两个字符串:string.lower(a) < string.lower(b)

      • 函数string.sub(s, i, j)从字符串 s 中提取第 i 个到第 j 个字符(包括第 i 个和第 j 个字符,字符串的第一个字符索引为 1)。该函数也支持负数索引,负数索引从字符串的结尾开始计数,Lua 语言中的字符串是不可变的。和 Lua 语言中的所有其他函数一样,函数string.sub()并不会改变原有字符串的值,它只会返回一个新字符串。一种常见的误解是以为string.sub(s, 2, -2)返回的是修改后的 s。如果需要修改原字符串,那么必须把新的值赋给它

      • 函数string.char()string.byte()用于转换字符以及其内部数值表示。函数string.char()接收零个或多个整数作为参数,然后将每个整数转换成对应的字符,最后返回这些字符连接而成的字符串。函数string.byte(s, i)返回字符串 s 中第 i 个字符的内部数值表示,该函数的第二个参数是可选的。调用string.byte(s)返回字符串 s 中的第一个字符(如果字符串只由一个字符组成,那么久返回这个字符)的内部数值表示。

      • 函数string.format()是用于进行字符串格式化和将数值输出为字符串的强大工具,该函数会返回第一个参数(也就是所谓的格式化字符串)的副本,其中的每一个指示符都会被替换为使用对应格式进行格式化后的对应参数。格式化字符串中的指示符与 C 语言中函数printf()的规则类似,一个指示符由一个百分号和一个代表格式化方式的字母组成。

        string.format 格式化参数表

        参数 含义
        %c 接受一个数字,并将其转化为ASCII码表中对应的字符
        %d, %i 接受一个数字并将其转化为有符号的整数格式
        %o 接受一个数字并将其转化为八进制数格式
        %u 接受一个数字并将其转化为无符号整数格式
        %x 接受一个数字并将其转化为十六进制数格式,使用小写字母
        %X 接受一个数字并将其转化为十六进制数格式,使用大写字母
        %e 接受一个数字并将其转化为科学计数法格式,使用小写字母e
        %E 接受一个数字并将其转化为科学计数法格式,使用大写字母E
        %f 接受一个数字并将其转化为浮点数格式
        %g(%G) 接受一个数字并将其转化为%e(%E,对应%G)即%f中较短的一种格式
        %q 接受一个字符串并将其转化为可安全被 Lua 编译器读入的格式
        %s 接受一个字符串并按照给定的参数格式化该字符串
      • 可以使用冒号操作符像调用字符串的一个方法那样调用字符串标准库中的所有函数。例如,string.sub(s, i, j)可以重写为 s:sub(i, j)string.upper(s)可以重写为 s:upper()

      • 函数string.find()用于在指定的字符串中进行模式搜索

      • 函数string.gsub()则把所有匹配的模式用另一个字符串替换

    • 练习

      --在字符串指定位置插入
      function insert( str, pos, insertStr )
      	local newStr = str:sub(1,pos-1)..insertStr..str:sub(pos,string.len(str))
      	print(newStr)
      end
      --在字符串指定位置删除
      function Remove( str, pos, len )
      	local newStr = str:sub(1,pos-1)..str:sub(pos+len,string.len(str))
      	print(newStr)
      end
      --判断回文
      function ispali( str )
      	str = str:gsub("%p","")
      	local len = #str
      	for i = len, len/2, -1 do
      		if str:byte(i) ~= str:byte(len - i +1) then
      			print("no")
      			return
      		end
      	end
      	print("yes")
      	return
      end
      
    • 表索引

      • 可以是不同类型的索引,也可以是对象
      • table["key"] 等价于 table.key
    • 表结构器

      • 表构造器时用来创建和初始化表的表达式,也是 Lua 语言中独有的也是最有用、最灵活的机制之一

        days = {"Sunday","Monday", "Tuesday", "Wednsday", "Thursday", "Friday", "Saturday"}
        
        a = {x = 10, y = 20}
        --等价于
        a = {}; a.x = 10; a.y = 20
        --不过,在第一种写法中,由于能够提前判断表的大小,所以运行速度更快。
        
      • 数组、列表和序列

        • 如果想表示常见的数组或者列表,那么只需要使用整型作为索引的表即可。同时,也不需要预先声明表的大小,只需要直接初始化我们需要的元素即可,不过,在 Lua 语言中,数组索引按照惯例时从 1 开始的,Lua 语言中的其他很多机制也遵循这个惯例。
        • Lua 语言提供了获取序列长度的操作符#。正如我们之前所看到的,对于字符串而言,该操作符返回字符串的字节数;对于表而言,该操作符返回表对应序列的长度。
        • 对于中间存在空洞的列表而言,序列长度操作符是不可靠的,它只能用于序列。更准确的说,序列是由指定的 n 个正数数值类型的键所组成的集合形成的表。特别的,不包含数值类型键的表就是长度为零的序列。
    • 遍历表

      • 我们可以使用pairs迭代器遍历表中的键值对,受限于表在 Lua 语言中的底层实现机制,遍历过程中元素的出现顺序可能是随机的,相同的程序在每次运行时也可能产生不同的顺序。唯一可以确定的是,在遍历的过程中每个元素会且只会出现一次。
      • 对于列表而言,可以使用ipairs迭代器,Lua 会确保遍历时按照顺序进行的。
      • 另一种遍历序列的方法是使用数值型 for 循环
    • 安全访问

      • 考虑如下的情景:我们向确认在指定的库中是否存在某个函数。如果我们确定这个库确实存在,那么可以直接使用 if lib.foo then ...;否则,就得使用形如 if lib and lib.foo then ...的表达式。
        当表的嵌套深度变得比较深时,这种写法就会很容易出错,例如

        zip = company and company.director and
                  company.director.address and
                    company.director.address.zipcode
        

        这种写法不仅冗长而且低效,该写法在一次成功的访问中对表进行了6次访问而非3次访问。
        对于这种情景,诸如 C# 的一些编程语言提供了一种安全访问操作符。在 C# 中,这种安全访问操作符被记为?.。例如,对于表达式a?.b,当 a 为 nil 时,其结果是 nil 而不会产生异常。使用这种操作符,可以将上例改为:

        zip = company?.director?.address?.zipcode
        

        如果上述成员访问过程中出现 nil,安全访问操作符会正确地处理 nil 并最终返回 nil。

        Lua 语言并没有提供安全访问操作符,并且认为也不应该提供这种操作符。一方面,Lua 语言在设计上力求简单;另一方面,这种操作符也是非常有争议的,很多人就无理由地认为该操作符容易导致无意地编程错误。不过,我们可以使用其他语句在 Lua 语言中模拟安全访问操作符

        对于表达式 a or {},当 a 为 nil 时其结果是一个空表。因此,对于表达式(a or {}).b,当 a 为 nil 时其结果也同样是 nil。这样,我们就可以将之前的例子重写为:

        zip = (((company or {}).director or {}).address or {}).zipcode
        
        --再进一步简化
        E = {}
        -- some codes
        zip = (((company or E).director or E).address or E).zipcode
        

        确实,上述语法比安全访问操作符更加复杂。不过尽管如此,表中的每一个字段名都只被使用了一次,从而保证了尽可能少地对表进行访问(本例中对表仅有三次访问)

    • 表标准库

      • table.insert()向序列的指定位置插入一个元素,其他元素依次后移。例如,对于列表t = {10, 20, 30},在调用table.insert(t, 1, 15)后它会变成{15, 10, 20, 30},另一种特殊但常见的情况是调用 insert 时不指定位置,此时该函数会在序列的最后插入指定的元素,而不会移动任何元素。

      • 函数table.remove删除并返回序列指定位置的元素,然后将其后的元素向前移动填充删除元素后造成的空洞。如果在调用该函数时不指定位置,该函数会删除序列的最后一个元素。

      • Lua 5.3 对于移动表中的元素引入了一个更通用的函数table.move(a, f, e, t),调用该函数可以将表 a 中从索引 f 到 e 的元素(闭区间)移动到位置t上。

        table.move(a, 1, #a, 2)
        a[1] = newElement
        
        table.move(a, 2, #, 1)
        a[#a] = nil
        
        --应该注意,在计算机领域,移动实际上是将一个值从一个地方拷贝到另一个地方。因此,像上面的例子一样,我们必须在移动后显式地把最后一个元素删除。
        

        函数table.move还支持使用一个表作为可选参数。当带有可选地表作为参数时,该函数将第一个表中地元素移动到第二个表中。例如,table.move(a, 1, #a, 1, {})返回列表 a 的一个克隆,table.move(a, 1, #a, #b + 1, b)将列表 a 中的所有元素复制到列表 b 的末尾。

      • table.concat ,该函数将指定表的字符串元素连接在一起,当多个字符串要组合链接的时候开销比..小多了

      • Lua 语言提供了函数table.pack。该函数像表达式 {...} 一样保存所有的参数,然后将其放在一个表中返回,但是这个表还有一个保存了参数个数的额外字段 “n”。例如,下面的函数使用了函数table.pack来检测参数中是否有 nil

        function nonils(...)
          local arg = table.pack(...)
          for i = 1, arg.n do
            if arg[i] == nil then return false end
          end
          return true
        end
        
        print(nonils(2, 3, nil))
        print(nonils(2, 3))
        print(nonils())
        print(nonils(nil))
        
      • 多重返回值还涉及一个特殊的函数table.unpack。该函数的参数是一个数组,返回值为数组的所有元素,顾名思义,函数table.unpack()与函数table.pack()的功能相反。pack 会把参数列表转换成 Lua 语言中一个真实的列表,而 unpack 则把 Lua 语言中真实的列表转换成一组返回值,进而可以作为一个函数的参数被使用

        unpack 函数的重要用途之一体现在泛型调用机制中。泛型调用机制允许我们动态地调用具有任意参数的任意函数。例如,在 ISO C 中,我们无法编写泛型调用的代码,只能声明可变长参数的函数或者使用函数指针来调用不同的函数。但是,我们仍然不能调用具有可变数量参数的函数,因为 C 语言中的每一个函数调用的实参个数是固定的,并且每个实参的类型也是固定的。而在 Lua 语言中,却可以做到这一点。如果我们想通过数组 a 传入可变的参数来调用函数 f,那么可以写成f(table.unpack(a))unpack 会返回 a 中所有的元素,而这些元素又被用作 f 的参数。例如,考虑如下代码:

        print(string.find("hello", "ll"))
        --可以使用如下的代码动态地构造一个等价地调用:
        f = string.find
        a = {"hello", "ll"}
        
        print(f(table.unpack(a)))
        

        通常,函数table.unpack()使用长度操作符获取返回值地个数,因而该函数只能用于序列。不过,如果有需要,也可以显式地限制返回元素的范围

        print(table.unpack({"Sun", "Mon", "Tue", "Wed"}, 2, 3)) --> "Mon" "Tue"

    • 练习

      --请编写一个函数,该函数用于测试指定的表是否为有效的序列
      function CheckSequence( tab )
      	if type(tab) ~= "table" then
      		print("no")
      		return
      	end
      	local len = #tab
      	local len2 = 0
      	for k,v in pairs(tab) do
      		len2 = len2 + 1
      	end
      	if len2 ~= len then
      		print("no")
      	else
      		print("yes")
      	end
      end
      --请编写一个函数,该函数将指定列表的所有元素插入到另一个列表指定的位置。
      function insertTab( tab, insertTab, pos )
      	if pos > #tab then
      		pos = #tab
      	end
      	local newTab = {}
      	table.move(tab, 1, pos, 1, newTab)
      	table.move(insertTab, 1, #insertTab, #newTab+1, newTab)
      	table.move(tab, pos + 1, #tab, #newTab+1, newTab)
      	return newTab
      end
      
    • 定义

      • 在 Lua 语言中,函数是对语句和表达式进行抽象的主要方式。函数既可以用于完成某种特定任务或子例程,也可以只是进行一些计算然后返回计算结果。在前一种情况下,我们将一句函数调用视为一条语句;而在后一种情况下,我们则将函数调用视为表达式

      • 无论哪种情况,函数调用时都需要使用一对圆括号把参数列表括起来。即使被调用的函数不需要参数,也需要一对空括号 ()。对于这个规则,唯一的例外就是,当函数有且只有一个参数,且该参数是字符串常量或表构造器时,括号是可选的

        print "hello world"    <-->    print("hello world")
        dofile 'a.lua'    <-->    dofile('a.lua')
        print [[amulti-line message]]    <-->    print([[amulti-line message]])
        f{x = 10, y =20}    <-->    f({x = 10, y = 20})
        type{}    <-->    type({})
        
      • Lua 语言也为面向对象风格的调用提供了一种特殊的语法,即冒号操作符。形如 o:foo(x)的表达式意为调用对象 o 的 foo 方法

    • 多返回值

    • Lua 语言中一种与众不同但又非常有用的特性是允许函数返回多个结果。Lua 语言中几个预定义函数就会返回多个值。Lua 语言编写的函数同样可以返回多个结果,只需在 return 关键字后列出要返回的值即可

    • Lua 语言根据函数的被调用情况调整返回值的数量。当函数被作为一条单独的语句调用时,其所有返回值都会被丢弃;当函数被作为表达式调用时,将只保留第一个返回值。只有当函数调用是一系列表达式中的最后一个表达式(或唯一一个表达式)时,其所有的返回值才能被获取到。这里所谓的“一系列表达式”在 Lua 中表现为 4 种情况:多重赋值、函数调用时传入的实参列表、表构造器和 return 语句

      function foo0() end
      function foo1() return "a" end
      function foo2() return "a", "b" end
      --多重赋值 在多重赋值中,如果一个函数没有返回值或者返回值个数不够多,那么 Lua 语言会用 nil 来补充缺失的值:
      x, y = foo2()    -->    x = "a", y = "b"
      x = foo2()    -->    x = "a", "b"被丢弃
      x, y, z = 10, foo2()    -->    x = 10, y = "a", z = "b"
      --函数调用
      print(foo0())    -->    (无结果)
      print(foo1())    -->    a
      print(foo2())    -->    a b
      print(foo2(), 1)    -->    a 1
      print(foo2() .. "x")    -->    ax
      print("x" .. foo2())    -->    xa
      --表构造器
      t = {foo0()}    -->    t = {}
      t = {foo1()}    -->    t = {"a"}
      t = {foo2()}    -->    t = {"a", "b"}
      --不过,这种行为只有当函数调用是表达式列表中的最后一个时才有效,在其他位置上的函数调用总是只返回一个结果:
      t = {foo0(), foo2(), 4}    -->    t[1] = nil, t[2] = "a", t[3] = 4
      --return语句
      function foo(i)
        if i == 0 then return foo0()
        elseif i == 1 then return foo1()
        elseif i == 2 then return foo2()
        end
      end
      print(foo(1))
      print(foo(2))
      print(foo(0))
      print(foo(3))
      --将函数调用用一对圆括号括起来也可以强制其只返回一个结果:
      print((foo0()))    -->    nil
      print((foo1()))    -->    a
      print((foo2()))    -->    a
      
    • 可变长参数函数

      • Lua 语言中的函数可以是可变长参数函数,即可以支持数量可变的参数。

        function add(...)
          local s = 0
          for _, v in ipairs{...} do
            s = s + v
          end
          return s
        end
        
        print(add(3, 4, 10, 25, 12))    -->    54
        
      • 参数列表中的三个点...表示该函数的参数是可变长的。当这个函数被调用时,Lua 内部会把它所有参数收集起来,我们把这些收集起来的参数称为函数的额外参数。当函数要访问这些参数时仍需用到三个点,但不同的是此时这三个点是作为一个表达式来使用的。

      • 操作技巧上可以参考类似C++的tuple一样分成一个和一包的使用如

        function fwrite(fmt, ...)
          return io.write(string.format(fmt, ...))
        end
        
      • Lua 语言提供了函数table.pack。该函数像表达式 {...} 一样保存所有的参数,然后将其放在一个表中返回.

      • 另一种遍历函数的可变长参数的方法是使用函数 select。函数 select 总是具有一个固定额参数 selector,以及数量可变的参数。如果 selector 是数值 n,那么函数 select 则返回第 n 个参数后的所有参数;否则,selector 应该是字符串 “#”,以便函数 select 返回额外参数的总数

        print(select(1, "a", "b", "c"))    -->    a, b, c
        print(select(2, "a", "b", "c"))    -->    b, c
        print(select(3, "a", "b", "c"))    -->    c
        print(select("#", "a", "b", "c"))    -->    3
        

        通常,我们在需要把返回值个数调整为 1 的地方使用函数 select,因此可以把select(n, ...)认为是返回第 n 个额外参数的表达式。
        来看一个使用函数 select 的典型示例,下面是使用该函数的 add 函数

        function add(...)
          local s = 0
          for i = 1, select("#", ...) do
            s = s + select(i, ...)
          end
          return s
        end
        

        对于参数较少的情况,第二个版本额 add 更快,因为该版本避免了每次调用时创建一个新表。不过,对于参数较多的情况,多次带有很多参数调用函数 select 会超过创建表的开销,因此第一个版本会更好(特别的,由于迭代的次数和每次迭代时传入参数的个数会随着参数的个数增长,因此第二个版本的时间开销是二次代价的)。

    • 正确的尾调用

      • Lua 语言中有关函数的另一个有趣的特性是,Lua 语言是支持尾调用消除的。这意味着 Lua 语言可以正确地尾递归,虽然尾递归调用消除的概念并没有直接涉及,递归尾调用是被当作函数调用使用的跳转。当一个函数的最后一个动作是调用另一个函数而没有再进行其他工作时,就形成了尾调用。例如function f(x) x = x + 1; return g(x) end代码中对函数 g 的调用就是尾调用

      • 当函数 f 调用完函数 g 后,f 不再需要进行其他的工作。这样,当被调用的函数执行结束后,程序就不再需要返回最初的调用者。因此,在尾调用之后程序也就不需要再调用栈钟保存有关调用函数的任何信息。当 g 返回时,程序的执行路径会直接返回到调用 f 的位置。在一些语言的实现中,例如 Lua 语言解释器,就利用了这个特点,使得在进行尾调用时不使用任何额外的栈空间。我们就将这种实现称为尾调用消除。

      • 由于尾调用不会使用栈空间,所以一个程序中能够嵌套的尾调用的数量是无限的。例如,下列函数支持任意数字作为参数

        function foo(n)
          if n > 0 then return foo(n - 1) end
        end
        --该函数永远不会发生栈溢出。
        
      • 关于尾调用消除的一个重点就是如何判断一个调用是尾调用。很多函数调用之所以不是尾调用,是由于这些函数在调用之后还进行了其他工作。例如,下例中调用 g 就不是尾调用:

        function f(x) 
          g(x) --当调用完 g 后,f 在返回前还不得不丢弃 g 返回的所有结果
        end
        return g(x) + 1 --处理加法
        return x or g(x)--处理逻辑运算
        return (g(x))--处理约束
        
        --在 Lua 语言中,只有形如return func(arg)的调用才是尾调用。不过,由于 Lua 语言会在调用前对 func 及其参数求值,所以 func 及其参数都可以是复杂的表达式。例如,下面的例子就是尾调用
        return x[i].foo(x[j] + a * b, i + j)
        
    • 练习

      --请编写一个函数,该函数的参数为一个数组,打印出该数组的所有元素。
      function MyPrintArr( arr )
      	for i,v in ipairs(arr) do
      		print(i,v)
      	end
      end
      --请编写一个函数,该函数的参数为可变数量的一组值,返回值为除第一个元素之外的其他所有值。
      function RemoveFrist( ... )
      	return select(2,...)
      end
      --请编写一个函数,该函数的参数为可变数量的一组值,返回值为除了最后一个元素之外的所有值。
      function RemoveLast( ... )
      	local args = table.pack(...)
      	args[#args] = nil
      	return table.unpack(args)
      end
      --请编写一个函数,该函数用于打乱一个指定的数组,请保证所有的排列都是等概率的。
      function ShuffleArr( arr )
      	local newTab = {}
      	local len = #arr
      	math.randomseed(os.time())
      	for i= 1 , len do
      		local idx = math.random(1,#arr)
      		table.insert(newTab,arr[idx])
      		table.remove(arr,idx)
      	end
      	return newTab
      end
      
      --请编写一个函数,其参数为一个数组,返回值为数组中元素的所有组合。
      --可以使用组合的递推公式 C(n, m) = C(n - 1, m - 1) + C(n - 1, m)。
      --要计算从 n 个元素中选出 m 个组成的组合 C(n, m),可以先将第一个元素加到结果集中,
      --然后计算所有的其他元素的C(n - 1, m - 1);然后,从结果中删掉第一个元素,再计算其他所有剩余元素的 C(n - 1, m)。当 n 小于 m 时,组合不存在;当 m 为 0 时,只有一种组合(一个元素也没有)。
      function Combination( arr )
      	local C = {}
      	for i=1,#arr do
      		C[i] = {}
      		for j=1,#arr do
      			C[i][j] = 0
      		end
      	end
      	C[1][0] = 1
      	C[1][1] = 1
      	for i=2,#arr do
      		C[i][0] = 1;
      		for j=1, #arr do
      			C[i][j] = (C[i - 1][j] + C[i - 1][j - 1])
      		end
      	end
      	return C
      end
      
      for i,v in ipairs(Combination({1,2,3})) do
      	for k,v2 in ipairs(v) do
      		print(i,k,v2)
      	end
      end
      
      --有时,具有正确尾调用的语句被称为 正确的尾递归,争论在于这种正确性只与递归调用有关(如果没有递归调用,那么一个程序的最大调用深度是静态固定的)。
      --请证明上述争论的观点在像 Lua 语言一样的动态语言中不成立:不使用递归,编写一个能够实现支持无限调用链的程序。
      --想要实现无限调用链,就要在完成函数功能以后再返回自身,即是像 C++ 运算符重载那样,可以无限地使用 << 来对 cout 进行输出
      function MyPrint( value )
      	print(value)
      	return MyPrint
      end
      
      MyPrint(1)(2)(3)
      
    • 所谓输入流就是指从文件流入程序,而输出流则是从程序流入文件

    • 简单模型

    • 对于文件操作来说,I/O 库提供了两种不同的模型。简单模型虚拟了一个 当前输入流 和一个 当前输出流,其 I/O 操作时通过这些流实现的。 I/O 库把当前输入流初始化为进程的标准输入,将当前输出流初始化为进程的标准输出。

    • 函数io.input和函数io.output可以用于改变当前的输入输出流。调用io.input(filename)会以只读模式打开指定文件,并将文件设置为当前输入流。之后,所有的输入都将来自该文件,除非再次调用io.input。对于输出而言,函数io.output的逻辑与之类似。如果出现错误,这两个函数都会抛出异常。如果想直接处理这些异常,则必须使用完整 I/O 模型。

    • 函数io.write可以读取任意数量的字符串(或者数字)并将其写入当前输出流。由于调用该函数时可以使用多个参数,因此应该避免使用io.write(a .. b .. c),应该调用io.write(a, b, c),后者可以用更少的资源达到同样的效果,并且可以避免更多的连接动作

      作为原则,应该只在 “用后即弃” 的代码或者调试代码中使用函数print;当需要完全控制输出时,应该使用函数io.write。与函数print不同,函数io.write不会在最终的输出结果中添加诸如制表符或换行符这样的额外内容。此外,函数io.write允许对输出进行重定向,而函数print只能使用标准输出。最后,函数print可以自动为其参数调用tostring,这一点对于调试而言非常便利,但也容易导致一些诡异的 Bug。

    • 函数io.read可以从当前输入流中读取字符串,其参数决定了要读取的数据:

      参数 含义
      "a" 读取整个文件
      "l" 读取下一行(丢弃换行符)
      "L" 读取下一行(保留换行符)
      "n" 读取一个值
      num 以字符串读取 num 个字符

      调用io.read("a")可从当前位置开始读取当前输入文件的全部内容。如果当前位置处于文件的末尾或文件为空,那么该函数返回一个空字符串。
      因为 Lua 语言可以高效地处理长字符串,所以在 Lua 语言中编写过滤器地一种简单技巧就是将整个文件读取到一个字符串中,然后对字符串进行处理,如:

      t = io.read("a")
      t = string.gsub(t, "bad", "good")
      io.write(t)
      
    • 不过,如果要逐行迭代一个文件,那么使用io.lines()迭代器会更简单

      local count = 0
      for line in io.lines() do
        count = count + 1
        io.write(string.format("%6d ", count), line, "\n")
      end
      
    • io.read(0)是一个特例,它常用于测试是否到达了文件末尾。如果仍然由数据可供读取,它会返回一个空字符串;否则,返回nil。

    • 完整 I/O 模型

      • 简单 I/O 模型对简单的需求而言还算适用,但对于诸如同时读写多个文件等高级的文件操作来说就不够了。对于这些文件操作,我们需要用到完整 I/O 模型。

      • 可以使用函数io.open 来打开一个文件,该函数仿造了 C 语言中的函数fopen。这个函数有两个参数,一个参数时待打开的文件名,另一个参数是模式字符串。模式字符串包括表示只读的 r、表示只写的 w、表示追加的 a,以及另外一个可选的表示打开二进制文件的 b。函数 io.open 返回对应文件的流。当发生错误时,该函数会在返回 nil 的同时返回一条错误信息和一个系统相关的错误码 print(io.open("non-existent-file", "r"))

      • 检查错误的一种典型方法是使用函数 assert:local f = assert(io.open(filename, mode))

      • 在打开文件后,可以使用方法readwrite从流中读取和向流中写入。它们与函数readwrite相似,但需要使用冒号运算符将它们当作流对象的方法来调用。

        local f = assert(io.open(filename, "r"))
        local t = f : read("a")
        f : close()
        
      • I/O 库提供了三个预定义的 C 语言流的句柄:io.stdin、io.stdout 和 io.stderr。例如,可以使用如下的代码将信息直接写到标准错误流中:io.stderr:write(message)

      • 函数io.inputio.output允许混用完整 I/O 模型和简单 I/O 模型。调用无参数的io.input()可以获得当前输入流,调用io.input(handle)可以设置当前输入流(类似的调用同样适用于函数 io.output)。例如,如果想要临时改变当前输入流,可以像这样

        local temp = io.input()
        io.input("newinput")
        -- do something
        io.input() : close()
        io.input(temp)
        --注意,io.read(args)实际上是io.input() : read(arg)的简写,即函数read是用在当前输入流上的。同样,io.write(args)是io.output() : write(args) 的简写。
        
      • 函数io.lines从流中读取内容。函数io.lines返回一个可以从流中不断读取内容的迭代器,给函数io.lines提供一个文件名,它就会以只读的方式打开对应文件的输入流,并在到达文件末尾关闭该输入流。若调用时不带参数,函数io.lines就从当前输入流读取。我们也可以把函数lines当作句柄的一个方法。此外,从 Lua 5.2 开始,函数 io.lines 可以接收和函数 io.read 一样的参数。例如,下面的代码会以在 8KB 为块迭代,将当前输入流中的内容复制到当前输出流中

        for block in io.input() : lines(2^13) do
          io.write(block)
        end
        
    • 其他文件操作

      • 函数io.tmpfile返回一个操作临时文件的句柄,该句柄是以读/写模式打开的。当程序运行结束以后,该临时文件会被自动移除(删除)。

      • 函数flush将所有缓冲数据写入文件。与函数write一样,我们也可以把它当作io.flush()使用,以刷新当前输出流;或者把它当作方法f : flush()使用,以刷新流 f。

      • 函数setvbuf用于设置流的缓冲模式。该函数的第一个参数是一个字符串:"no" 表示无缓冲,"full" 表示在缓冲区满时或者显式地刷新文件时才写入数据,"Line" 表示输出一直被缓冲直到遇到换行符或从一些特定文件中读取到了数据。对于后两个选项,函数setvbuf支持可选地第二参数,用于指定缓冲区大小。

        在大多数系统中,标准错误流(io.stderr)是不缓冲的,而标准输出流(io.stdout)按行缓冲。因此,当向标准输出中写入了不完整的行(比如进度条)时,可能需要刷新这个输出流才能看到输出结果。

      • 函数seek用来获取和设置文件的当前位置,常常使用f : seek(whence, offset)的形式来调用,其中参数 whence 时一个指定如何使用偏移的字符串,当参数 whence 取值为 "set" 时,表示相对于文件开头的偏移;取值为 "cur" 时,表示相对于文件当前位置的偏移;取值为 "end" 时,表示相对于文件尾部的偏移。不管 whence 的取值是什么,该函数都会以字节为单位,返回当前新位置在流中相对于文件开头的偏移

        whence 的默认值是 "cur",offset 的默认值是 0。因此,调用函数file : seek()会返回当前的位置且不改变当前位置;调用函数file : seek("set")会将位置重置到文件开头并返回 0。调用函数file : seek("end")会将当前位置重置到文件结尾并返回文件的大小。下面的函数演示了如何在不修改当前位置的情况下获取文件大小

        function fsize(file)
          local current = file : seek()
          local size = file : seek("end")
          file : seek("set", current)
          return size
        end
        
      • 此外,函数os.rename用于文件重命名,函数os.remove用于移除文件。需要注意的是,由于这两个函数处理的是真实文件而非流,所以它们位于 os 库而非 io 库中。

    • 其他系统调用

      • 函数os.exit用于终止程序的执行。该函数的第一个参数是可选的,表示该程序的返回状态,其值可以为一个数值(0 表示执行成功)或者一个布尔值(true 表示执行成功);该函数的第二个参数也是可选的,当值为 true 时会关闭 Lua 状态并调用所有析构器释放所占用的所有内存(这种终止方式通常是非必要的,因为大多数操作系统会在进程退出时释放其占用的所有资源)。
      • 函数os.getenv用于获取某个环境变量,该函数的输入参数是环境变量的名称,返回值为保存了该环境变量对应值的字符
    • 运行系统命令

      • 函数os.execute用于运行系统命令,它等价于 C 语言中的函数 system。该函数的参数为表示待执行命令的字符串,返回值为命令结束后的状态。其中,第一个返回值是一个布尔类型,当为 true 时表示程序成功运行完成;第二个返回值是一个字符串,当为 "exit" 时表示程序正常运行结束,当为 "signal" 时表示因信号而中断;第三个返回值是返回状态(若该程序正常终结)或者终结该程序的信号代码。例如,在 POSIX 和 Windows 中都可以使用如下的函数创建新目录

        function createDir(dirname)
          os.execute("mkdir ".. dirname)
        end
        
      • 另一个非常有用的函数是io.popen。同函数os.execute一样,该函数运行一条系统命令,但该函数还可以重定向命令的输入/输出,从而使得程序可以向命令中写入或从命令的输出中读取,例如,下列代码使用当前目录中的所有内容构建了一个表:

        local f = io.popen("dir /B", "r")
        local dir = {}
        for entry in f : lines() do
          dir[#dir + 1] = entry
        end
        --其中,函数io.popen的第二个参数 "r" 表示从命令的执行结果中读取。由于该函数的默认行为就是这样,所以在上例中的参数实际是可选的。
        
    • 读取性能

      • 性能从高到低:按块 --> 一次性读取 --> 按行 --> 按字节
      • 输入文件最大支持2的31次方 - 1
    • 练习

      --请编写一个程序,该程序读取一个文本文件然后将每行的内容按照字母表顺序排序后重写该文件。如果在调用时不带参数,则从标准输入读取并向标准输出写入;
      --如果在调用时传入一个文件名作为参数。则从该文件中读取并向标准输出写入;如果在调用时传入两个文件名作为参数,则从第一个文件读取并将结果写入到第二个文件中。
      --请改写上面的程序,使得当指定的输出文件已经存在时,要求用户进行确认。
      function ReadFile( readFileName, outputFileName )
      	local sortTab = {}
      	if readFileName == nil then
      		for line in io.lines() do
      		 	table.insert(sortTab,line)
      		 end
      	elseif readFileName ~= nil and outputFileName == nil then
      		for line in io.lines(readFileName) do
      		 	table.insert(sortTab,line)
      		 end
      	else
      		for line in io.lines(readFileName) do
      		 	table.insert(sortTab,line)
      		 end
      		local out = io.open(outputFileName, "r")
      		if out then
      			print("文件已存在")--我编译器不是在交互模式跑的,就不写交互版的了,原理一样
      			return
      		else
      			io.output(outputFileName)
      		end
      	end
      	table.sort(sortTab)
      	io.write(table.concat( sortTab, "\n"))
      end
      
      
      --请编写一个程序,该程序输出一个文本文件的最后一行。当文件较大且可以使用 seek 时,请尝试避免读取整个文件。
      --请将上面的程序修改得更加通用,使其可以输出一个文本文件的最后 n 行。同时,当文件较大且可以使用 seek 时,请尝试避免读取整个文件。
      function ReadLastLine( readFileName ,lastCount)
      	local file = io.open(readFileName, "r")
      	local lastCount = (lastCount or 1) * 2
      	local char
      	if file then
      		local start = file:seek("set")
      		local size = file:seek("end")
      		for i = size - 1,start,-1 do
      			file:seek("set",i)
      			char = file:read(1)
      			if char == "\n" then
      				lastCount = lastCount - 1
      				if lastCount == 0 then
      					print(file:read("l"))
      					file:close()
      					return
      				end
      			end
      		end
      		file:close()
      	end
      end
      
      --使用函数os.execute和io.popen,分别编写用于创建目录、删除目录和输出目录内容的函数。
      function CreateDir(dirname)
        os.execute("mkdir ".. dirname)
      end
      
      function DeleteDir(dirname)
        os.execute("rmdir /s/q ".. dirname)
      end
      
      function PrintDir()
      	local f = io.popen("dir ../", "r")
      	f:read("a")
      end
      
    • 局部变量和代码块

      • Lua 语言中的变量在默认情况下是全局变量,所有的局部变量在使用前必须声明。与全局变量不同,局部变量的生效范围仅限于声明它的代码块。一个代码块是一个控制结构的主体,或是一个函数的主体,或是一个代码段(即变量被声明时所在的文件或字符串)
      • 尽可能地使用局部变量是一种良好的编程风格。首先,局部变量可以避免由于不必要的命名而造成全局变量的混乱;其次,全局变量还能避免同一程序中不同代码部分中的命名冲突;再次,访问局部变量比访问全局变量更快;最后,局部变量会随着其作用域的结束而消失,从而使得垃圾收集器能够将其释放。
      • Lua 语言中有一种常见的用法:local foo = foo 这段代码声明了一个局部变量 foo,然后用全局变量 foo 对其赋初值(局部变量 foo 只有在声明之后才能被访问)。这个用法在需要提高对 foo 的访问速度时很有用。当其他函数改变了全局变量 foo 的值,而这段代码又需要保留原始值时,这个用法很有用,尤其是在进行运行时动态替换时。即使其他代码把 print 动态替换成了其他函数,在local print = print语句之前的所有代码使用的都还是原先的print函数。
    • 控制结构

      • Lua 语言提供了一组精简且常用的控制结构,包括用于条件执行的 if 以及用于循环的 while、repeat 和 for。所有的控制结构语法上都有一个显式的终结符:end 用于终结 if、for 及 while 结构,until 用于 repeat 结构

      • if then else

      if 语句先测试其条件,并根据条件是否满足执行相应的 then 部分或 else 部分。else 部分是可选的。

      • while

        顾名思义,当条件为真时 while 循环会重复执行其循环体。Lua 语言先测试 while 语句的条件,若条件为假则循环结束;否则,Lua会执行循环体并不断地重复这个过程。

      • repeat

      repeat-until 语句会重复执行其循环体直到条件为真时才结束。由于条件测试在循环体之后执行,所以循环体至少会执行一次。(其他语言的do while)

      • 数值型 for和泛型 for
    • break、return 和 goto

      • break 和 return 语句从当前的循环结构中跳出,goto 语句则允许跳转到函数中的几乎任何地方。

参考资料

posted @ 2021-02-07 19:08  陌冉  阅读(434)  评论(0编辑  收藏  举报