函數
1. 基礎知識
調用函數都需要寫圓括號,即使沒有參數,但有一種特殊例外:函數若只有一個參數且參數是字面字符串或table構造式,則圓括號可有可無,如dofile 'a.lua',f{x=10, y=20}。
Lua為面向對象式的調用提供冒號操作符的特殊語法,如o.foo(o, x)等價于o:foo(x)。和Javascript類似,調用函數時提供的實參數量可以與形參數量不同,若實參多了則舍棄,不足則多余的形參初始化為nil。
1.1 多重返回值
Lua允許函數返回多個結果,函數返回如return max, index,接收如s, e = string.find("hello Lua world", "Lua")。如果一個函數調用不是一系列表達式的最后一個元素,則只產生一個值:
function foo() return "a", "b" end
x, y = foo(), 20 -- x="a", y=20(foo的第二個返回值被丟棄)
print(foo() .. "x") -- 輸出ax,這是因為當函數出現在一個表達式中時,Lua會將其返回值數量調整為1
另外,只有當一個函數調用作為最后一個元素時,返回值才不會被調整,在其他位置都會被調整為1個,如t = {foo2()}則t={“a”, “b”},t = {foo2(), 4}則t={“a”, 4}。
特殊函數unpack接受一個數組作為參數,并從下標1開始返回該數組的所有元素,如a, b = unpack({10, 20, 30}),則30被丟棄。unpack的一項重要用途體現在“泛型調用”機制中。
1.2 變長參數
函數參數表中3個點(…)表示該函數可接受不同數量的實參。在Lua 5.0中,沒有提供“…”表達式,如果要遍歷變長參數,可以訪問函數內隱含的局部變量arg。如果還有固定參數,則必須放在變長參數之前。
2. 高級主題
2.1 closure閉合函數
和Javascript的閉包基本是一個東西,此處不再贅述。從技術上說,Lua中只有closure,而不存在“函數”,因為函數本身就是一種特殊的closure。closure的應用很廣泛,如用于高階函數的參數、為GUI工具包創建回調、重定義函數并在新實現中調用舊實現、創建“沙盒”安全運行環境等等。
2.2 非全局的函數
大部分Lua庫都采用了將函數存儲在table中的機制(如io.read,math.sin),例如下面采用了三種方式來定義table的成員函數:
MathLib = {
plus = function(x, y) return x + y end
}
MathLib.minus = function(x, y) return x - y end
function MathLib.multiply(x, y) return x * y end
局部函數的定義:
local f = function(參數>) 函數體> end
local function f(參數>) 函數體> end -- Lua提供的語法糖
**注意如果定義遞歸函數,不能使用上面第一種定義方式(因為在函數體調用f時,f尚未定義完畢),使用第二種“語法糖”則沒問題;或者使用“前向聲明”,先local f再f = function ...這樣定義。
2.3 正確的尾調用
當一個函數調用時另一個函數的最后一個動作時,該調用算是一條“尾調用”,例如function f(x) return g(x) end。由于在尾調用后程序不要保存任何關于該函數的棧信息,所以遞歸調用不會耗費棧空間,可以遞歸調用無數次。有一些看似是“尾調用”的代碼,其實都違背了這條準則:
function f(x) g(x) end -- 調用g后,f沒有立即返回,還需要丟棄g返回的臨時結果
function f(x) return g(x) + 1 -- 還要做一次加法
function f(x) return x or g(x) -- 必須調整為一個返回值
所以,只有形如return func>(args>)這樣的調用形式才算是尾調用。
面向對象編程
Lua中的table就是一種對象,因為它和對象一樣可以擁有狀態,也擁有一個獨立于其值的標識(一個self),也和對象一樣具有獨立于創建者的生命周期。但是Lua中沒有類的概念,只能用元表來實現原型,用原型來模擬類和繼承等面向對象特性。本文將介紹Lua關于面向對象編程的內容。
1 self與冒號語法
使用self參數是所有面向對象語言的一個核心,Lua只需使用冒號語法,就能隱藏該參數,例如下面兩段代碼是等價的。
Account = {balance=0}
funtion Account.withdraw(self, v)
self.balance = self.balance - v
end
a1 = Account; Account = nil
a1.withdraw(a1, 100.0) -- 注意這是可以運行的
function Account:withdraw(v)
self.balance = self.balance - v
end
a2 = Account
a2:withdraw(100.0) -- 省略了a2參數傳入
2 類的編寫
在一些基于原型的語言中,對象是沒有類型的,但每個對象都有一個原型。原型是一種常規的對象,當其他對象遇到一個未知操作時,原型會先查找它。在這種語言中要表示一個類,只需創建一個專用做其他對象的原型。Lua中實現原型很簡單,只需用元表的__index來實現繼承。
(當訪問一個table中不存在的字段key時,一般得到結果為nil。事實上,訪問會促使解釋器去查找一個叫__index的元方法,如果沒有這個元方法,則訪問結果如前述的nil,否則由這個元方法來提供結果。元方法除了是一個函數,還可以是一個table,如果是table則直接返回該table中key對應的內容。)
如果有兩個對象a和b,要讓b作為a的一個原型,只需setmetatable(a, {__index=b})。a就會在b中查找它沒有的操作。
function Account:new(o)
o = o or {} -- 如果用戶沒有提供table,則創建一個
setmetatable(o, self)
self.__index = self
return o
end
當調用a = Account:new{balance = 0}時,a會將Account(函數中的self)作為其元表。當調用a:withdraw(100.0)時,Lua無法在table a中找到條目withdraw,則進一步搜索元表的__index條目,即getmetatable(a).__index.withdraw(a, 100.0)。由于new方法中做了self.__index = self,所以上面的表達式又等價于Account.withdraw(a, 100.0),這樣就傳入了a作為self參數,又調用了Account類的withdraw函數。這種創建對象的方式不僅可以作用于方法,還可以作用于所有其他新對象中沒有的字段。
3 繼承
現在要從Account類派生出一個子類SpecialAccount(以使客戶能夠透支),只需:
SpecialAccount = Account:new()
s = SpecialAccount:new{limit=1000.00}
SpecialAccount從Account繼承了new,當執行SpecialAccount:new時,其self參數為SpecialAccount,因此s的元表為SpecialAccount。當調用s不存在的字段時,會向上查找,也可以編寫新的重名方法覆蓋父類方法。
4 多重繼承
上面介紹中為__index元方法賦值一個table實現了單繼承,如果要實現多重繼承,可以讓__index字段成為一個函數,在該函數中搜索多個基類的方法字段。由于這種搜索具有一定復雜性,多重繼承的性能不如單一繼承。還有一種改進性能的簡單做法是將繼承的方法復制到子類中,但這種做法的缺點是當系統運行后就較難修改方法的定義,因為這些修改不會沿著繼承體系向下傳播。
5 私密性
Lua在設計對象時,沒有提供私密性機制(private),但其各種元機制使得程序員可以模擬對象的訪問控制。這種實現不常用,因此只做基本的了解:通過兩個table來表示一個對象,一個用來保存對象的狀態,一個用于對象的操作(即接口)。
function newAccount(initialBalance)
local self = {balance = initialBalance}
local withdraw = function(v)
self.balance = self.balance -v
end
return {
withdraw = withdraw
}
end
通過閉包的方式,將具有私密性的字段(如balance)保存在self table中,并只公開了withdraw接口,這樣就能實現私密性機制。
您可能感興趣的文章:- Lua中的string庫(字符串函數庫)總結
- Lua中的函數(function)、可變參數、局部函數、尾遞歸優化等實例講解
- Lua中的一些常用函數庫實例講解
- Lua中的模塊與module函數詳解
- Lua中的函數知識總結
- Lua字符串庫中的幾個重點函數介紹
- Lua的table庫函數insert、remove、concat、sort詳細介紹
- Lua中的常用函數庫匯總
- Lua中的面向對象編程詳解
- Lua面向對象之類和繼承
- Lua面向對象之多重繼承、私密性詳解
- Lua面向對象編程學習筆記