ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
## 变量声明与 C 语言的不同 Lua 中有一个常见的用法,不论变量、函数都可以用下面这种方法保存到局部变量中(同时加快访问速度): ~~~ local foo = foo ~~~ 书里加了个括号来解释这种写法: > The local foo becomes visible only after its declaration. 这一点需要瞎扯的是 C 语言里相应的东西。 ~~~ int foo = 12; int bar = 6; void foobar(void) { int foo = foo; int bar[bar]; } ~~~ 与 Lua 不同,在 C 语言中初始赋值是声明之后的事情。所以这里函数 foobar 中的 foo 会被初始化为自己(而不是全局的 foo,所以值不确定),bar 却被合法地定义为一个含有 6 个元素的数组。 ## 看似多余的限制 另一个有趣的现象是在 4.4 节中说到: > For syntactic reasons, a break or return can appear only as the last statement of a block; in other words, as the last statement in your chunk or just before an end, an else, or an until. 乍一看觉得加上这个限制真是麻烦,但想想这不正是 break/return 的正确用法么?因为其后的语句都永远不会被执行到,所以如果不是在块的最后写 break/return 是毫无意义的(调试除外)。虽然看上去是挺多余的一段话,但也算是说出了事物的本源。 ## 函数的本质 第六章 More About Functions 中说到我们平时在 Lua 中写的函数声明 ~~~ function foo (x) return 2*x end ~~~ 其实是一种语法糖,本质上我们可以把它写成如下代码: ~~~ foo = function (x) return 2*x end ~~~ 于是也就可以说 * Lua 中的所有函数都是匿名函数,之前所谓「具名函数」只是保存了某个匿名函数的变量罢了。 * Lua 中的函数声明其实只是一个语句而已。 ## 终于有用的知识 在第 47 页看到了一段令人泪流满面的代码和运行结果: ~~~ function derivative (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 ~~~ 最初我并不知道 derivative 是什么意思,但看了示例代码和运行结果,顿时恍然大悟:这货不就是导数吗? ## 沙盒 ### 背景知识 Lua 给我的感觉是:各种内置函数和标准库的存在感都是比较强的。如果执行这句: ~~~ for name in pairs(_G) do print(_G) end ~~~ 就会把各种环境中已存在名称的打印出来: * 全局变量:比如字符串 _VERSION。 * 内置函数:比如 print、tonumber、dofile 之类。 * 模块名称:比如 string、io、coroutine 之类。 这里的全局变量 _G 就是存放环境的表(于是会有 _G 中存在着 _G._G 的递归)。 于是,平时对于全局变量的访问就可以等同于对 _G 表进行索引: ~~~ value = _G[varname] --> value = varname _G[varname] = value --> varname = value ~~~ ### 改变函数的环境 函数的上下文环境可以通过 setfenv(f, table) 函数改变,其中 table 是新的环境表,f 表示需要被改变环境的函数。如果 f 是数字,则将其视为堆栈层级(Stack Level),从而指明函数(1 为当前函数,2 为上一级函数): ~~~ a = 3 -- 全局变量 a setfenv(1, {}) -- 将当前函数的环境表改为空表 print(a) -- 出错,因为当前环境表中 print 已经不存在了 ~~~ 没错,不仅是 a 不存在,连 print 都一块儿不存在了。如果需要引用以前的 print 则需要在新的环境表中放入线索: ~~~ a = 3 setfenv(1, { g = _G }) g.print(a) -- 输出 nil g.print(g.a) -- 输出 3 ~~~ ### 沙盒 于是,出于安全或者改变一些内置函数行为的目的,需要在执行 Lua 代码时改变其环境时便可以使用 setfenv 函数。仅将你认为安全的函数或者新的实现加入新环境表中: ~~~ local env = {} -- 沙盒环境表,按需要添入允许的函数 function run_sandbox(code) local func, message = loadstring(code) if not func then return nil, message end -- 传入代码本身错误 setfenv(func, env) return pcall(func) end ~~~ ### Lua 5.2 的 _ENV 变量 Lua 5.2 中所有对全局变量 var 的访问都会在语法上翻译为 _ENV.var。而 _ENV 本身被认为是处于当前块外的一个局部变量。(于是只要你自己定义一个名为 _ENV 的变量,就自动成为了其后代码所处的「环境」(enviroment)。另有一个「全局环境」(global enviroment)的概念,指初始的 _G 表。) Lua 的作者之一 Roberto Ierusalimschy 同志在介绍 Lua 5.2 时说: > the new scheme, with _ENV, allows the main benefit of setfenv with a little more than syntactic sugar. 就我的理解来说,优点就是原先虚无缥缈只能通过 setfenv、getfenv 访问的所谓「环境」终于实体化为一个始终存在的变量 _ENV 了。 于是以下两个函数内容大致是一样的: ~~~ -- Lua 5.1 function foobar() setfenv(1, {}) -- code here end -- Lua 5.2 function foobar() local _ENV = {} -- code here end ~~~ 而更进一步的是,5.2 中对 load 函数作出了修改。(包括但不限于 :))合并了 loadstring 功能,并可以在参数中指定所使用的环境表: ~~~ local func, message = load(code, nil, "t", env) ~~~ ## 面向对象 没错,Lua 中只存在表(Table)这么唯一一种数据结构,但依旧可以玩出面向对象的概念。 ### 添加成员函数 好吧,如果熟悉 C++ 还是很好理解类似的进化过程的:如果说 struct 里可以添加函数是从 C 过渡到 C++ 的第一认识的话,为 Table 添加函数也可以算是认识 Lua 是如何面向对象的第一步吧。 ~~~ player = { health = 200 } --> 一个普通的 player 表,这里看作是一个对象 function takeDamage(self, amount) self.health = self.health - amount end takeDamage(player, 20) --> 调用 ~~~ 如何将独立的 takeDamage 塞进 player 中咧?答案是直接定义进去: ~~~ player = { health = 200 } function player.takeDamage(self, amount) self.health = self.health - amount end player.takeDamage(player, 20) --> 调用 ~~~ 这样就相当于在 player 表中添加了一个叫做 takeDamage 的字段,和下面的代码是一样的: ~~~ player = { health = 200, takeDamage = function(self, amount) --> Lua 中的函数是 first-class value self.health = self.health - amount end } player.takeDamage(player, 20) --> 调用 ~~~ 调用时的 player.takeDamage(player, 20) 稍显不和谐(据说用术语叫做 DRY),于是就要出动「冒号操作符」这个专门为此而生的语法糖了: ~~~ player:takeDamage(20) --> 等同于 player.takeDamage(player, 20) function player:takeDamage(amount) --> 等同于 function player.takeDamage(self, amount) ~~~ ### 从对象升华到类 类的意义在于提取一类对象的共同点从而实现量产(我瞎扯的 >_<)。同样木有 Class 概念的 Javascript 使用 prototype 实现面向对象,Lua 则通过 Metatable 实现与 prototype 类似的功能。 ~~~ Player = {} function Player:create(o) --> 参数 o 可以暂时不管 o = o or { health = 200 } --> Lua 的 or 与一般的 || 不同,如果非 nil 则返回该非 nil 值 setmetatable(o, self) self.__index = self return o end function Player:takeDamage(amount) self.health = self.health - amount end playerA = Player:create() --> 参数 o 为 nil playerB = Player:create() playerA:takeDamage(20) playerB:takeDamage(40) ~~~ 顾名思义 Metatable 也是一个 Table,可以通过在其中存放一些函数(称作 metamethod)从而修改一些默认的求值行为(如何显示为字符串、如何相加、如何连接、如何进行索引)。Metatable 的 __index 域设置了「如何进行索引」的方法。例如调用 foo.bar 时,如果在 foo 中没有找到名为 bar 的域时,则会调用 Metatable:__index(foo, bar)。于是: ~~~ playerA:takeDamage(20) ~~~ 因为在 playerA 中并不存在 takeDamge 函数,于是求助于 Metatable: ~~~ getmetatable(playerA).__index.takeDamage(playerA, 20) ~~~ 带入 Metatable 后: ~~~ Player.__index.takeDamage(playerA, 20) ~~~ 因为 Player 的 __index 在 create 时被指定为 self,所以最终变为: ~~~ Player.takeDamage(playerA, 20) ~~~ 于是 takeDamage 的 self 得到了正确的对象 playerA。 ### 继承 继承是面向对象的一大特性,明白了如何创建「类」,那么继承也就比较明了了,还记得大明湖畔的参数 o 么? ~~~ RMBPlayer = Player:create() function RMBPlayer:broadcast(message) --> 为子类添加新的方法 print(message) end function RMBPlayer:takeDamage(amount) --> 子类重载父类方法 self.health = self.health - amount / (self.money / 100) end vip = RMBPlayer:create { money = 200 } --> 子类添加新成员(单个 Table 作为参数可以省略括号) vip:takeDamage(20) vip:broadcast("F*ck") ~~~ 以上便是 Lua 中实现面向对象的基本方法。