k = {}k[2]="hello"k["haha"]="world"print(k[2],k["haha"]) --> 显示:hello world
遍历 table
虽然 table 可以用下标来赋值和读取内容,但作为一种“key-value 表”,肯定是不够的。Lua 也提供了一批功能更完全的函数,帮我们做更多的事情。我们先来看看如何遍历一个 table,循环语法中的 for 专门为了这个设计了一个写法,以及一个专门的内置函数 pairs():
a={}a[1]="one"a["second"]="two"a[78]="three"a["last"]="four"for k,v in pairs(a) do print(k,v)end
运行结果是:
1 onelast four78 threesecond two
用 for k,v in pairs(...) do 的写法,可以很方便的遍历并且提取整个 table 的内容。在早期的一些语言中,没有把 for 和语言常用的数据结构结合,只能通过下标来操作数据结构,代码写起来就非常麻烦。如果要遍历一个哈希表之类的结构,先要获得一个哈希表的“迭代器”对象,然后通过调用迭代器身上的方法(或者迭代函数)才能遍历。由于 table 的下标可以是各种类型的值,所以要简单的获得 table 的记录总数,一般只能通过遍历来计算,而没有什么特别简单的方法。
多态:支持多个不同类型(定义)的对象,只要它们都满足其中一种类型定义,就可以“作为”这类对象来使用。这种特性最常用于取代复杂的 switch...case 语法,做法就是为不同类型的对象,都定义同一个名字的方法,这些不同类型的对象,都可以被调用这个相同名字的方法,但执行的代码则会被自动调用那些应该属于那种类型的方法。等于自动以对象的类型为 switch 的选择,每个对象的方法为 case 的代码段。
为了完成上面的几种任务,不同的语言使用了不同的设计。JAVA/C++/C#/Python 这类语言选择的是所谓“对象模板”的方案,其特征就是语言中会有 class 关键字。开发者通过定义各种 class 来设计对象的内容,在运行时通过这些 class 实例化出很多对象来使用。这种做法的好处是,class 的设计非常明确且固定,在复杂的逻辑下比较不容易写错代码。但是缺点是使用这些 class 的时候比较死板和啰嗦,每个对象都必须和他的“模板”一模一样,如果有任何一点不同,就需要定义一个新的 class,如果不仔细设计,很容出现所谓“类爆炸”的情况——一个程序里面定义的 class 实在太多了,导致开发者已经分辨不过来了。这种设计对于“多态”的实现上,尤其复杂,为了让不同“类”的对象能作为同一个类来使用,就必须运行对象同时继承多个类,而同时继承多个类又比较容易出现逻辑冲突,还要设计类似“接口”这种特殊的,专门用于多态的类,总之就是越搞越复杂。
有些语言要求用户手工进行内存管理,所以每个变量都会需要选择是建立在“栈”还是“堆”上,并且用户写代码去回收“堆”上的变量——C语言就是这种。而 Java、C# 这类语言,由于代码是在一个“虚拟机”里面运行的,这个“虚拟机”程序会跟踪记录“堆”上的变量,并且自己找时间去清理那些无用的变量,所以关键字里面只有 new 而没有 delete——这对于开发者来说尤其友好。像 Go 这种语言,则连 new 关键字都不存在,只要返回一个“函数内的局部变量”,也就是“栈”上的变量,就自动会编译成堆上的变量,并且标记为需要跟踪清理。在程序的运行中,Go 写的程序也会自动回收“无用”的堆变量。Lua 和很多脚本语言一样,由于是通过解析器运行,所以可以认为所有的变量,都是解析器在管理,因此也无需关注到底是“堆”变量还是“栈”变量,总之会自动进行垃圾回收就可以,所以既没有 new 也没有 delete。
上面的代码会打印出结果 11 20,可以看到,变量 a 作为全局变量,在函数 f() 里面被修改了内容,而且还增加了一个全局变量 b。这对于习惯于其他语言的开发者来说,是非常“坑爹”的设计,因为其他语言变量默认都是局部的。默认是全局变量很容易导致比较大型的项目中,全局变量的名字“碰撞”到重名,导致莫名其妙的内容就被改掉了。所以请一定记住尽量多用 local 关键字,下面是正确示范:
a = 10function f() local a = 11 local b = 20endf()print(a,b) -- 打印结果:10 nil
-- 此函数会抛出错误function add(a,b) assert(type(a) == "number", "a is not a number") if type(b) ~= "number" then error("b is not a number") end return a+bend-- 通过 pcall() 捕获错误rs, err = pcall(add, 1, "kk")print(rs, err)rs, err = pcall(add, "ww", 2)print(rs, err)rs, err = pcall(add, 1, 2)print(rs, err)
代码运行结果:
false test.lua:4: b is not a numberfalse test.lua:2: a is not a numbertrue 3
运行时错误,作为一种特殊的“函数返回”,一直是编程语言不得不面对的问题。Lua 其实和 C++ C# 一样,都是采用的自愿处理原则,也就是程序员可以不处理,但程序最终会中断。而 Java 采用的是强迫处理原则,如果一个函数声明了可能抛出异常,调用者必须处理这些异常,否则代码编译不过——这个做法确实能强迫开发者编写错误处理代码,但代价就是程序写起来费劲很多。而 Go 和 C 语言则显得非常佛系,你可以选择处理返回值里面代表错误的变量,也可以忽略,程序依然会继续运行,直到一个开发者无法预料的结果。这三种设计到底哪种最好,其实并无定论。你也可以在 Lua/C++/C# 这样的语言中,使用 GO 和 C 语言的错误处理策略,但重要的是必须要处理各种错误而不是漏掉。
多任务支持
现代计算机系统,基本上都支持“同时”运行多个程序。除了操作系统上,我们可以启动多个进程同时运行,我们编写程序,也往往会需要在一个程序里面处理多个事情,譬如游戏里面,我们需要一边让画面上的角色活动,一边等待和处理玩家的输入;我们在加载或者处理一个大型文件内容的时候,同时需要在屏幕上显示正在处理的进度信息;网络服务器程序,需要同时处理多个玩家的操作等等。——所有这些需要同时运行的代码,实际上是对计算机 CPU 的运行时间分配和管理提出了需求。如何解决这个需求,不同的计算机语言也提出了不同的方案。
通过 C 语言,我们能利用 Linux 操作系统的 fork() 等系统调用函数,比较方便生成子进程,这样当前运行的函数,就会在两个进程里“同时”运行了。但是这种机制有很多问题,譬如这两个进程运行的函数都是同一个,虽然可以通过写代码的方式在后续的运行中再选择不同的代码,但是用起来也太麻烦了点。另外,操作系统的子进程建立和销毁,从性能上来说还有改进的余地。所以后来也出现了 pthread 库这种“多线程”的功能库,让使用者可以直接启动任何一个需要“同时”运行的函数,到后来的 JAVA C# 以及更新版本的 C++,都对多线程进行了更好的支持。
为了解决上述的问题,另外一个解决多任务的思路,就是不试图去同时运行几个函数。而是让函数可以运行到某些代码之后,就切换到运行其他函数,当条件满足之后,在切换回来继续运行。这种做法比较典型的是,当函数运行到一个需要 CPU 等待的操作的时候,譬如等待读取网络、等待读取文件、等待用户输入这类操作的函数之后,就切换出去运行其他需要“同时”运行的函数上,直到之前在“等待”的事情有结果了,再切换回来继续运行这些代码。
在使用 Lua 的协程时,必须要注意所有的函数,都必须是非阻塞的,因为只有一个线程,所以任何一个阻塞就会把所有协程都阻塞了。因此对于网络 IO 等操作,就要用类似 epoll 这类异步 API。同步的阻塞 API 只能在 C 语言那侧启动多线程,然后再发起回调到 Lua 这一侧。
框架支持
随着软件项目的越来越大,很多时候我们写的代码,并不仅仅是给具体的用户来使用的,而是会为其他开发者也写很多代码。这种提供给开发者使用的代码,是通过代码的各种编程接口来提供的。最简单的就是我们写了一个函数,然后提供开发者调用。这些函数如果有很多,我们会整理成为一个集合,称之为“函数库”(Library)。库里面的有一部分函数,是为了实现库的功能而存在,是不希望被开发者使用的,而另外一部分,则是设计用来专门提供给开发者使用的——这些函数我们称之为 API(Application Programming Interface)。但是现在 API 这个名词已经被庸俗化的限定成 RESTful API 的含义了,让我很不爽。
除了 API 以外,还有一种提供开发者使用的代码方式,就是“框架”(Framework)。所谓框架,就是让开发者按某个规则来写一段代码,放到我(框架开发者)的代码里面来运行。这是和 API 库一样历史悠久。写过 C/JAVA 程序的人都知道,程序的启动都需要写一个 main() 函数,而函数的参数就是命令行参数,返回值会被操作系统记录为进程的结束状态。这就是一种“框架”的设计,main() 的格式就是一种框架的接口要求,启动进程这个过程就是框架在工作,然后调用使用者的代码。
-- 框架代码:从命令行参数执行命令function run_cmd() -- arg 是一个全局 table 变量,记录了全部的命令行参数 if not arg[1] or not arg[2] or not arg[3] then print("Usage: cmd arg1 arg2") return end local f=load("return "..arg[1].."("..arg[2]..","..arg[3]..")") print(f())end-- 回调函数function add(a, b) return a+b endfunction sub(a, b) return a-b end-- 运行框架run_cmd()
运行示例:
$ lua test.lua add 1 34$ lua test.lua sub 1 3-2
在上面的例子中,我们直接用函数名字 add, sub 作为指令进行调用,关键是调用了load()这个函数,这个函数可以解析传入的字符串,按照 Lua 代码的格式进行处理,并且把这段代码构造成一个函数返回。如果我们输入命令lua test.lua add 1 3,实际上动态的构造了一个如下的函数:
-- local f=load("return ".."add".."(".."1"..",".."3"..")")local f = function(...) return add(1,3) end
现在大部分语言的源码,都不会仅仅放在一个文件里面。那么如何才能让多个不同的源文件,合并到一起工作呢?这就是所谓“模块管理”要解决的问题。很多时候,我们可以把一个源文件视为一个“模块”,那么这个源文件要能作为模块被其他源文件使用,必须要遵守一定的规则。不同的语言对于这个问题有不同的设计,比较奇葩的是 C 语言就没有对此有任何的标准的规定,可能是因为这门语言实在是太古老了。
评论区
共 2 条评论热门最新