Lua 中有一個(gè)常見(jiàn)的用法,不論變量、函數(shù)都可以用下面這種方法保存到局部變量中(同時(shí)加快訪問(wèn)速度):
local foo = foo
書里加了個(gè)括號(hào)來(lái)解釋這種寫法:
The local foo becomes visible only after its declaration.
這一點(diǎn)需要瞎扯的是 C 語(yǔ)言里相應(yīng)的東西。
int foo = 12;
int bar = 6;
void foobar(void)
{
int foo = foo;
int bar[bar];
}
與 Lua 不同,在 C 語(yǔ)言中初始賦值是聲明之后的事情。所以這里函數(shù) foobar 中的 foo 會(huì)被初始化為自己(而不是全局的 foo,所以值不確定),bar 卻被合法地定義為一個(gè)含有 6 個(gè)元素的數(shù)組。
另一個(gè)有趣的現(xiàn)象是在 4.4 節(jié)中說(shuō)到:
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.
乍一看覺(jué)得加上這個(gè)限制真是麻煩,但想想這不正是 break/return 的正確用法么?因?yàn)槠浜蟮恼Z(yǔ)句都永遠(yuǎn)不會(huì)被執(zhí)行到,所以如果不是在塊的最后寫 break/return 是毫無(wú)意義的(調(diào)試除外)。雖然看上去是挺多余的一段話,但也算是說(shuō)出了事物的本源。
第六章 More About Functions 中說(shuō)到我們平時(shí)在 Lua 中寫的函數(shù)聲明
function foo (x) return 2*x end
其實(shí)是一種語(yǔ)法糖,本質(zhì)上我們可以把它寫成如下代碼:
foo = function (x) return 2*x end
于是也就可以說(shuō)
在第 47 頁(yè)看到了一段令人淚流滿面的代碼和運(yùn)行結(jié)果:
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 是什么意思,但看了示例代碼和運(yùn)行結(jié)果,頓時(shí)恍然大悟:這貨不就是導(dǎo)數(shù)嗎?
Lua 給我的感覺(jué)是:各種內(nèi)置函數(shù)和標(biāo)準(zhǔn)庫(kù)的存在感都是比較強(qiáng)的。如果執(zhí)行這句:
for name in pairs(_G) do print(_G) end
就會(huì)把各種環(huán)境中已存在名稱的打印出來(lái):
這里的全局變量 _G 就是存放環(huán)境的表(于是會(huì)有 _G 中存在著 _G._G 的遞歸)。
于是,平時(shí)對(duì)于全局變量的訪問(wèn)就可以等同于對(duì) _G 表進(jìn)行索引:
value = _G[varname] --> value = varname
_G[varname] = value --> varname = value
函數(shù)的上下文環(huán)境可以通過(guò) setfenv(f, table) 函數(shù)改變,其中 table 是新的環(huán)境表,f 表示需要被改變環(huán)境的函數(shù)。如果 f 是數(shù)字,則將其視為堆棧層級(jí)(Stack Level),從而指明函數(shù)(1 為當(dāng)前函數(shù),2 為上一級(jí)函數(shù)):
a = 3 -- 全局變量 a
setfenv(1, {}) -- 將當(dāng)前函數(shù)的環(huán)境表改為空表
print(a) -- 出錯(cuò),因?yàn)楫?dāng)前環(huán)境表中 print 已經(jīng)不存在了
沒(méi)錯(cuò),不僅是 a 不存在,連 print 都一塊兒不存在了。如果需要引用以前的 print 則需要在新的環(huán)境表中放入線索:
a = 3
setfenv(1, { g = _G })
g.print(a) -- 輸出 nil
g.print(g.a) -- 輸出 3
于是,出于安全或者改變一些內(nèi)置函數(shù)行為的目的,需要在執(zhí)行 Lua 代碼時(shí)改變其環(huán)境時(shí)便可以使用 setfenv 函數(shù)。僅將你認(rèn)為安全的函數(shù)或者新的實(shí)現(xiàn)加入新環(huán)境表中:
local env = {} -- 沙盒環(huán)境表,按需要添入允許的函數(shù)
function run_sandbox(code)
local func, message = loadstring(code)
if not func then return nil, message end -- 傳入代碼本身錯(cuò)誤
setfenv(func, env)
return pcall(func)
end
Lua 5.2 中所有對(duì)全局變量 var 的訪問(wèn)都會(huì)在語(yǔ)法上翻譯為 _ENV.var。而 _ENV 本身被認(rèn)為是處于當(dāng)前塊外的一個(gè)局部變量。(于是只要你自己定義一個(gè)名為 _ENV 的變量,就自動(dòng)成為了其后代碼所處的「環(huán)境」(enviroment)。另有一個(gè)「全局環(huán)境」(global enviroment)的概念,指初始的 _G 表。)
Lua 的作者之一 Roberto Ierusalimschy 同志在介紹 Lua 5.2 時(shí)說(shuō):
the new scheme, with _ENV, allows the main benefit of setfenv with a little more than syntactic sugar.
就我的理解來(lái)說(shuō),優(yōu)點(diǎn)就是原先虛無(wú)縹緲只能通過(guò) setfenv、getfenv 訪問(wèn)的所謂「環(huán)境」終于實(shí)體化為一個(gè)始終存在的變量 _ENV 了。
于是以下兩個(gè)函數(shù)內(nèi)容大致是一樣的:
-- Lua 5.1
function foobar()
setfenv(1, {})
-- code here
end
-- Lua 5.2
function foobar()
local _ENV = {}
-- code here
end
而更進(jìn)一步的是,5.2 中對(duì) load 函數(shù)作出了修改。(包括但不限于 :))合并了 loadstring 功能,并可以在參數(shù)中指定所使用的環(huán)境表:
local func, message = load(code, nil, "t", env)
沒(méi)錯(cuò),Lua 中只存在表(Table)這么唯一一種數(shù)據(jù)結(jié)構(gòu),但依舊可以玩出面向?qū)ο蟮母拍睢?/p>
好吧,如果熟悉 C++ 還是很好理解類似的進(jìn)化過(guò)程的:如果說(shuō) struct 里可以添加函數(shù)是從 C 過(guò)渡到 C++ 的第一認(rèn)識(shí)的話,為 Table 添加函數(shù)也可以算是認(rèn)識(shí) Lua 是如何面向?qū)ο蟮牡谝徊桨伞?/p>
player = { health = 200 } --> 一個(gè)普通的 player 表,這里看作是一個(gè)對(duì)象
function takeDamage(self, amount)
self.health = self.health - amount
end
takeDamage(player, 20) --> 調(diào)用
如何將獨(dú)立的 takeDamage 塞進(jìn) player 中咧?答案是直接定義進(jìn)去:
player = { health = 200 }
function player.takeDamage(self, amount)
self.health = self.health - amount
end
player.takeDamage(player, 20) --> 調(diào)用
這樣就相當(dāng)于在 player 表中添加了一個(gè)叫做 takeDamage 的字段,和下面的代碼是一樣的:
player = {
health = 200,
takeDamage = function(self, amount) --> Lua 中的函數(shù)是 first-class value
self.health = self.health - amount
end
}
player.takeDamage(player, 20) --> 調(diào)用
調(diào)用時(shí)的 player.takeDamage(player, 20) 稍顯不和諧(據(jù)說(shuō)用術(shù)語(yǔ)叫做 DRY),于是就要出動(dòng)「冒號(hào)操作符」這個(gè)專門為此而生的語(yǔ)法糖了:
player:takeDamage(20) --> 等同于 player.takeDamage(player, 20)
function player:takeDamage(amount) --> 等同于 function player.takeDamage(self, amount)
類的意義在于提取一類對(duì)象的共同點(diǎn)從而實(shí)現(xiàn)量產(chǎn)(我瞎扯的 >_<)。同樣木有 Class 概念的 Javascript 使用 prototype 實(shí)現(xiàn)面向?qū)ο?,Lua 則通過(guò) Metatable 實(shí)現(xiàn)與 prototype 類似的功能。
Player = {}
function Player:create(o) --> 參數(shù) o 可以暫時(shí)不管
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() --> 參數(shù) o 為 nil
playerB = Player:create()
playerA:takeDamage(20)
playerB:takeDamage(40)
顧名思義 Metatable 也是一個(gè) Table,可以通過(guò)在其中存放一些函數(shù)(稱作 metamethod)從而修改一些默認(rèn)的求值行為(如何顯示為字符串、如何相加、如何連接、如何進(jìn)行索引)。Metatable 的 index 域設(shè)置了「如何進(jìn)行索引」的方法。例如調(diào)用 foo.bar 時(shí),如果在 foo 中沒(méi)有找到名為 bar 的域時(shí),則會(huì)調(diào)用 Metatable:index(foo, bar)。于是:
playerA:takeDamage(20)
因?yàn)樵?playerA 中并不存在 takeDamge 函數(shù),于是求助于 Metatable:
getmetatable(playerA).__index.takeDamage(playerA, 20)
帶入 Metatable 后:
Player.__index.takeDamage(playerA, 20)
因?yàn)?Player 的 __index 在 create 時(shí)被指定為 self,所以最終變?yōu)椋?/p>
Player.takeDamage(playerA, 20)
于是 takeDamage 的 self 得到了正確的對(duì)象 playerA。
繼承是面向?qū)ο蟮囊淮筇匦?,明白了如何?chuàng)建「類」,那么繼承也就比較明了了,還記得大明湖畔的參數(shù) 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 } --> 子類添加新成員(單個(gè) Table 作為參數(shù)可以省略括號(hào))
vip:takeDamage(20)
vip:broadcast("F*ck")
以上便是 Lua 中實(shí)現(xiàn)面向?qū)ο蟮幕痉椒ā?/p>
更多建議: