• 函数与面向对象
    • 变量声明与 C 语言的不同
    • 看似多余的限制
    • 函数的本质
    • 终于有用的知识
  • 沙盒
    • 背景知识
    • 改变函数的环境
    • 沙盒
    • Lua 5.2 的 _ENV 变量
  • 面向对象
    • 添加成员函数
    • 从对象升华到类
    • 继承
  • 导航

    函数与面向对象

    变量声明与 C 语言的不同

    Lua 中有一个常见的用法,不论变量、函数都可以用下面这种方法保存到局部变量中(同时加快访问速度):

    1. local foo = foo

    书里加了个括号来解释这种写法:

    The local foo becomes visible only after its declaration.

    这一点需要瞎扯的是 C 语言里相应的东西。

    1. int foo = 12;
    2. int bar = 6;
    3. void foobar(void)
    4. {
    5. int foo = foo;
    6. int bar[bar];
    7. }

    与 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 中写的函数声明

    1. function foo (x) return 2*x end

    其实是一种语法糖,本质上我们可以把它写成如下代码:

    1. foo = function (x) return 2*x end

    于是也就可以说

    • Lua 中的所有函数都是匿名函数,之前所谓「具名函数」只是保存了某个匿名函数的变量罢了。
    • Lua 中的函数声明其实只是一个语句而已。

    终于有用的知识

    在第 47 页看到了一段令人泪流满面的代码和运行结果:

    1. function derivative (f, delta)
    2. delta = delta or 1e-4
    3. return function (x)
    4. return (f(x + delta) - f(x))/delta
    5. end
    6. end
    7. c = derivative(math.sin)
    8. print(math.cos(10), c(10))
    9. --> -0.83907152907645 -0.83904432662041

    最初我并不知道 derivative 是什么意思,但看了示例代码和运行结果,顿时恍然大悟:这货不就是导数吗?

    沙盒

    背景知识

    Lua 给我的感觉是:各种内置函数和标准库的存在感都是比较强的。如果执行这句:

    1. for name in pairs(_G) do print(name) end

    就会把各种环境中已存在名称的打印出来:

    • 全局变量:比如字符串 _VERSION。
    • 内置函数:比如 print、tonumber、dofile 之类。
    • 模块名称:比如 string、io、coroutine 之类。

    这里的全局变量 _G 就是存放环境的表(于是会有 _G 中存在着 _G._G 的递归)。

    于是,平时对于全局变量的访问就可以等同于对 _G 表进行索引:

    1. value = _G[varname] --> value = varname
    2. _G[varname] = value --> varname = value

    改变函数的环境

    函数的上下文环境可以通过 setfenv(f, table) 函数改变,其中 table 是新的环境表,f 表示需要被改变环境的函数。如果 f 是数字,则将其视为堆栈层级(Stack Level),从而指明函数(1 为当前函数,2 为上一级函数):

    1. a = 3 -- 全局变量 a
    2. setfenv(1, {}) -- 将当前函数的环境表改为空表
    3. print(a) -- 出错,因为当前环境表中 print 已经不存在了

    没错,不仅是 a 不存在,连 print 都一块儿不存在了。如果需要引用以前的 print 则需要在新的环境表中放入线索:

    1. a = 3
    2. setfenv(1, { g = _G })
    3. g.print(a) -- 输出 nil
    4. g.print(g.a) -- 输出 3

    沙盒

    于是,出于安全或者改变一些内置函数行为的目的,需要在执行 Lua 代码时改变其环境时便可以使用 setfenv 函数。仅将你认为安全的函数或者新的实现加入新环境表中:

    1. local env = {} -- 沙盒环境表,按需要添入允许的函数
    2. function run_sandbox(code)
    3. local func, message = loadstring(code)
    4. if not func then return nil, message end -- 传入代码本身错误
    5. setfenv(func, env)
    6. return pcall(func)
    7. 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 了。

    于是以下两个函数内容大致是一样的:

    1. -- Lua 5.1
    2. function foobar()
    3. setfenv(1, {})
    4. -- code here
    5. end
    6. -- Lua 5.2
    7. function foobar()
    8. local _ENV = {}
    9. -- code here
    10. end

    而更进一步的是,5.2 中对 load 函数作出了修改。(包括但不限于 :))合并了 loadstring 功能,并可以在参数中指定所使用的环境表:

    1. local func, message = load(code, nil, "t", env)

    面向对象

    没错,Lua 中只存在表(Table)这么唯一一种数据结构,但依旧可以玩出面向对象的概念。

    添加成员函数

    好吧,如果熟悉 C++ 还是很好理解类似的进化过程的:如果说 struct 里可以添加函数是从 C 过渡到 C++ 的第一认识的话,为 Table 添加函数也可以算是认识 Lua 是如何面向对象的第一步吧。

    1. player = { health = 200 } --> 一个普通的 player 表,这里看作是一个对象
    2. function takeDamage(self, amount)
    3. self.health = self.health - amount
    4. end
    5. takeDamage(player, 20) --> 调用

    如何将独立的 takeDamage 塞进 player 中咧?答案是直接定义进去:

    1. player = { health = 200 }
    2. function player.takeDamage(self, amount)
    3. self.health = self.health - amount
    4. end
    5. player.takeDamage(player, 20) --> 调用

    这样就相当于在 player 表中添加了一个叫做 takeDamage 的字段,和下面的代码是一样的:

    1. player = {
    2. health = 200,
    3. takeDamage = function(self, amount) --> Lua 中的函数是 first-class value
    4. self.health = self.health - amount
    5. end
    6. }
    7. player.takeDamage(player, 20) --> 调用

    调用时的 player.takeDamage(player, 20) 稍显不和谐(据说用术语叫做 DRY),于是就要出动「冒号操作符」这个专门为此而生的语法糖了:

    1. player:takeDamage(20) --> 等同于 player.takeDamage(player, 20)
    2. function player:takeDamage(amount) --> 等同于 function player.takeDamage(self, amount)

    从对象升华到类

    类的意义在于提取一类对象的共同点从而实现量产(我瞎扯的 >_<)。同样木有 Class 概念的 Javascript 使用 prototype 实现面向对象,Lua 则通过 Metatable 实现与 prototype 类似的功能。

    1. Player = {}
    2. function Player:create(o) --> 参数 o 可以暂时不管
    3. o = o or { health = 200 } --> Lua or 与一般的 || 不同,如果非 nil 则返回该非 nil
    4. setmetatable(o, self)
    5. self.__index = self
    6. return o
    7. end
    8. function Player:takeDamage(amount)
    9. self.health = self.health - amount
    10. end
    11. playerA = Player:create() --> 参数 o nil
    12. playerB = Player:create()
    13. playerA:takeDamage(20)
    14. playerB:takeDamage(40)

    顾名思义 Metatable 也是一个 Table,可以通过在其中存放一些函数(称作 metamethod)从而修改一些默认的求值行为(如何显示为字符串、如何相加、如何连接、如何进行索引)。Metatable 的 index 域设置了「如何进行索引」的方法。例如调用 foo.bar 时,如果在 foo 中没有找到名为 bar 的域时,则会调用 Metatable:index(foo, bar)。于是:

    1. playerA:takeDamage(20)

    因为在 playerA 中并不存在 takeDamge 函数,于是求助于 Metatable:

    1. getmetatable(playerA).__index.takeDamage(playerA, 20)

    带入 Metatable 后:

    1. Player.__index.takeDamage(playerA, 20)

    因为 Player 的 __index 在 create 时被指定为 self,所以最终变为:

    1. Player.takeDamage(playerA, 20)

    于是 takeDamage 的 self 得到了正确的对象 playerA。

    继承

    继承是面向对象的一大特性,明白了如何创建「类」,那么继承也就比较明了了,还记得大明湖畔的参数 o 么?

    1. RMBPlayer = Player:create()
    2. function RMBPlayer:broadcast(message) --> 为子类添加新的方法
    3. print(message)
    4. end
    5. function RMBPlayer:takeDamage(amount) --> 子类重载父类方法
    6. self.health = self.health - amount / (self.money / 100)
    7. end
    8. vip = RMBPlayer:create { money = 200 } --> 子类添加新成员(单个 Table 作为参数可以省略括号)
    9. vip:takeDamage(20)
    10. vip:broadcast("F*ck")

    以上便是 Lua 中实现面向对象的基本方法。

    导航

    • 目录
    • 上一章:环境与模块
    • 下一章:标准库