Lua面向對象編程詳解

前言

Lua并非嚴格意義上的面向對象語言,在語言層面上并沒有直接提供諸如class這樣的關鍵字,也沒有顯式的繼承語法和virtual函數,但Lua提供了一種創建這些面向對象要素的能力。

Lua面向對象編程簡述

Lua中的table就是一種對象
1.table與對象一樣可以擁有狀態
2.table也是對象一樣擁有一個獨立于其值的標識(一個self)
3.table與對象一樣具有獨立于創建者和創建地的生命周期
先看看以下代碼:

local tab1 = {a = 1, b = 2}
local tab2 = {a = 1, b = 2}
if tab1 == tab2 then
    print("tab1 == tab2")
else
    print("tab1 ~= tab2")   -->tab1 ~= tab2
end 

上述說明兩個具有相同值的對象(table)是兩個不同的對象。對象有其自己的操作,同樣table也有這些操作:

Account = {balance = 0}
function Account.withdraw( v )
     Account.balance = Account.balance - v
end
Account.withdraw(100.00)
print(Account.balance)   -->-100

a = Account
Account = nil
a.withdraw(100.00)  --> attempt to index global 'Account' (a nil value)

上面的代碼創建了一個新函數,并將該函數存入Account對象的withDraw字段中,然后我們就可以調用該函數了。不過,這個特定對象還必須存儲在特定的全局變量中。如果改變了對象的名稱,withDraw就再也不能工作了,出現attempt to index global 'Account' (a nil value)
這種行為違反了前面提到的對象特性,即對象擁有獨立的生命周期。而我們可以通過增加一個參數來表示接受者,這個參數通常稱為self或this:

Account = {balance = 0}
function Account.withdraw( self, v )
    self.balance = self.balance - v
end
--基于修改后代碼的調用:
a1 = Account
Account = nil 
a1.withdraw(a1,100.00)
--正常工作。
print(a1.balance)  -->-100 

使用self參數是所有面向對象語言的一個核心。大多數面向對象語言都對程序員隱藏了self參數, Lua只需要使用冒號,則能隱藏該參數,重寫為:

function Account:withdraw( v )
    self.balance = self.balance - v
end

調用時可寫為:

a1:withdraw(100.00)

冒號的作用是在方法定義中添加一個額外的隱藏參數,以及在一個方法調用中添加一個額外的實參。冒號只是一種語法便利,并沒有引入任何新的東西。
現在的對象已有一個標識、狀態和狀態之上的操作。不過還缺乏一個類系統、繼承和私密性。

一個類就像是一個創建對象的模具。在Lua中則沒有類的概念,不過要在Lua中去模擬類并不困難。在Lua中,要表示一個類,只需創建一個專用作其他對象的原型。原型也是一種常規的對象,也就是說我們可以直接通過原型去調用對應的方法。當其它對象(類的實例)遇到一個未知操作時,原型會先查找它。
在Lua中實現原型是非常簡單的,比如有兩個對象a和b,要讓b作為a的原型,只需要以下代碼就可以完成:

setmetatable(a, {__index = b})

在此以后,a就會在b中查找所有它沒有的操作。若將b稱為是對象a的“類”,就僅僅是術語上的變化。現在我就從最簡單的開始,要創建一個實例對象,必須要有一個原型,就是所謂的“類”,看以下代碼:

local Account = {} -- 一個原型

好了,現在有了原型,那如何使用這個原型創建一個“實例”呢?接著看以下代碼:

--new可看作為構造函數
function Account:new(o)  
     o = o or {}  -- 如果用戶沒有提供table,則創建一個
     setmetatable(o, self)
     self.__index = self
     return o
end

當調用Account:new時,self就相當于Account。接著,我們就可以調用Account:new來創建一個實例了。再看:

local a = Account:new{balance = 100} -- 這里使用原型Account創建了一個對象
a:deposit(100.00)

上面這段代碼是如何工作的呢?首先使用Account:new創建了一個新的實例對象,并將Account作為新的實例對象a的元表。再當我們調用a:deposit(100.00)函數時,就相當于a.deposit(a),冒號就只是一個“語法糖”,只是一種方便的寫法。我們創建了一個實例對象a,當調用deposit時,就會查找a中是否有deposit字段,沒有的話,就去搜索它的元表,所以,最終的調用情況如下:

getmetatable(a).__index.deposit(a, 100.00)

a的元表是Account,Account的__index也是Account。因此,上面的調用也可以使這樣的:

Account.deposit(a, 100.00)

所以,其實我們可以看到的是,實例對象a表中并沒有deposit方法,而是繼承自Account方法的,但是傳入deposit方法中的self確是a。這樣就可以讓Account(這個“類”)定義操作。除了方法,a還能從Account繼承所有的字段。

繼承不僅可以用于方法,還可以作用于字段。因此,一個類不僅可以提供方法,還可以為實例中的字段提供默認值。看以下代碼:

 --[[ 
 在這段代碼中,我們可以將Account視為class的聲明,如Java中的: 
 public class Account  
 { 
    public float balance = 0; 
    public Account(Account o); 
    public void deposite(float f); 
 } 
--這里balance是一個公有的成員變量。
 --]]
local Account = {balance = 0}

--new可以視為構造函數
function Account:new(o) 
     o = o or {}  -- 如果用戶沒有提供table,則創建一個
     setmetatable(o, self)
     self.__index = self
     return o
end

function Account:deposit()
     self.balance = self.balance + 100
     print(self.balance)
end

local b = Account:new{} -- 這里使用原型Account創建了一個對象b
b:deposit() -->100
b:deposit() -->200

在Account表中有一個balance字段,默認值為0;當我創建了實例對象b時,并沒有提供balance字段,在deposit函數中,由于b中沒有balance字段,就會查找元表Account,最終得到了Account中balance的值,等號右邊的self.balance的值就來源自Account中的balance。調用b:deposit()時,其實就調用以下代碼:

b.deposit(b)

在deposit的定義中,就會變成這樣子:

b.deposit = getmetatable(b).__index.balance + 100

第一次調用deposit時,等號左側的self.balance就是b.balance,就相當于在b中添加了一個新的字段balance;當第二次調用deposit函數時,由于b中已經有了deposit字段,所以就不會去Account中尋找deposit字段了。

繼承

由于類也是對象(準確地說是一個原型),它們也可以從其它類(原型)獲得(繼承)方法。這種行為就是繼承,可以很容易的在Lua中實現。
假設有一個基類Account:

local Account = {balance = 0}

function Account:new(o)
     o = o or {}
     setmetatable(o, self)
     self.__index = self
     return o
end

function Account:deposit( v )
    self.balance = self.balance + v
end

function Account:withdraw( v )
    if v > self.balance then error("insufficient funds") end 
    self.balance = self.balance - v
end

現在需要從這個Account類派生出一個子類SpecialAccount ,則需要創建一個空的類,從基類繼承所有的操作:

SpecialAccount = Account:new()

現在,我創建了一個Account類的一個實例對象,在Lua中,現在SpecialAccount 既是Account類的一個實例對象,也是一個原型,就是所謂的類,就相當于SpecialAccount 類繼承自Account類。再如下面的代碼:

s = SpecialAccount:new{limit=1000.00}

SpecialAccount 從Account繼承了new;不過,在執行SpecialAccount :new時,它的self參數表示為SpecialAccount ,所以s的元表為SpecialAccount ,SpecialAccount 中字段__index的值也是SpecialAccount 。然后,我們就會看到,s繼承自SpecialAccount ,而SpecialAccount 又繼承自Account。當執行s:deposit(100.00)時,Lua在s中找不到deposit字段,就會查找SpecialAccount ;如果仍然找不到deposit字段,就查找Account,最終會在Account中找到deposit字段。可以這樣想一下,如果在SpecialAccount 中存在了deposit字段,那么就不會去Account中再找了。所以,我們就可以在SpecialAccount 中重定義deposit字段,從而實現特殊版本的deposit函數。

多重繼承

實現單繼承時,依靠的是為子類設置metatable,設置其metatable為父類,并將父類的__index設置為其本身的技術實現的。而多繼承也是一樣的道理,在單繼承中,如果子類中沒有對應的字段,則只需要在一個父類中尋找這個不存在的字段;而在多重繼承中,如果子類沒有對應的字段,則需要在多個父類中尋找這個不存在的字段。

Lua會在多個父類中逐個的搜索deposit字段。這樣,我們就不能像單繼承那樣,直接指定__index為某個父類,而是應該指定__index為一個函數,在這個函數中指定搜索不存在的字段的規則。這樣便可實現多重繼承。這里就出現了兩個需要去解決的問題:

保存所有的父類;
指定一個搜索函數來完成搜索任務。

-- 在多個父類中查找字段k
local function search(k, pList)
    for i = 1, #pList do
        local v = pList[i][k]
        if v then
            return v
        end
    end
end

function createClass(...)
    local c = {} -- 新類
    local parents = {...}

    -- 類在其元表中搜索方法
    setmetatable(c, {__index = function (t, k) return search(k, parents) end})

    -- 將c作為其實例的元表
    c.__index = c

    -- 為這個新類建立一個新的構造函數
    function c:new(o)
        o = o or {}
        setmetatable(o, self)

        -- self.__index = self 這里不用設置了,在上面已經設置了c.__index = c
        return o
    end

    -- 返回新的類(原型)
    return c
end

-- 一個簡單的類CA
local CA = {}
function CA:new(o)
    o = o or {}
    setmetatable(o, {__index = self})
    self.__index = self
    return o
end

function CA:setName(strName)
    self.name = strName
end

-- 一個簡單的類CB
local CB = {}
function CB:new(o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end

function CB:getName()
    return self.name
end

-- 創建一個c類,它的父類是CA和CB
local c = createClass(CA, CB)

-- 使用c類創建一個實例對象
local objectC = c:new{name = "Paul"}

-- 設置objectC對象一個新的名字
objectC:setName("John")
local newName = objectC:getName()
print(newName)

使用createClass創建了一個類(原型),將CA和CB設置為這個類(原型)的父類(原型);在創建的這個類(原型)中,設置了該類的__index為一個search函數,在這個search函數中尋找在創建的類中沒有的字段;
創建的新類中,有一個構造函數new;這個new和之前的單繼承中的new區別不大,很好理解;
調用new構造函數,創建一個實例對象,該實例對象有一個name字段;
調用object:setName(“John”)語句,設置一個新的名字;但是在objectC中沒有這個字段,怎么辦?好了,去父類找,先去CA找,一下子就找到了,然后就調用了這個setName,setName中的self指向的是objectC;設置以后,就相當于修改了objectC字段的name值;
調用objectC:getName(),objectC還是沒有這個字段。找吧,CA也沒有,那就接著找,在CB中找到了,就調用getName,在getName中的self指向的是objectC。所以,在objectC:getName中返回了objectC中name的值,就是“John”。

私密性

我們都知道,在C++或Java中,對于類中的成員函數或變量都有訪問權限的。public,protected和private這幾個關鍵字還認識吧。那么在Lua中呢?Lua中是本身就是一門“簡單”的腳本語言,本身就不是為了大型項目而生的,所以,它的語言特性中,本身就沒有帶有這些東西,那如果非要用這樣的保護的東西,該怎么辦?我們還是“曲線救國”。思想就是通過兩個table來表示一個對象。一個table用來保存對象的私有數據;另一個用于對象的操作。對象的實際操作時通過第二個table來實現的。為了避免未授權的訪問,保存對象的私有數據的表不保存在其它的table中,而只是保存在方法的closure中。看一段代碼:

function newObject(defaultName)
     local self = {name = defaultName}
     local setName = function (v) self.name = v end
     local getName = function () return self.name end
     return {setName = setName, getName = getName}
end

local objectA = newObject("John")
objectA.setName("John") -- 這里沒有使用冒號訪問
print(objectA.getName())

這種設計給予存儲在self table中所有東西完全的私密性。當調用newObject返回以后,就無法直接訪問這個table了。只能通過newObject中創建的函數來訪問這個self table;也就相當于self table中保存的都是私有的,外部是無法直接訪問的。大家可能也注意到了,我在訪問函數時,并沒有使用冒號,這個主要是因為,我可以直接訪問的self table中的字段,所以是不需要多余的self字段的,也就不用冒號了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容

  • 1.1程序塊:Lua執行的每段代碼,例如一個源代碼文件或者交互模式中輸入的一行代碼,都稱為一個程序塊 1.2注釋:...
    c_xiaoqiang閱讀 2,607評論 0 9
  • 第一篇 語言 第0章 序言 Lua僅讓你用少量的代碼解決關鍵問題。 Lua所提供的機制是C不擅長的:高級語言,動態...
    testfor閱讀 2,706評論 1 7
  • 2.5 面向對象編程 來源:2.5 Object-Oriented Programming 譯者:飛龍 協議:...
    布客飛龍閱讀 778評論 0 34
  • 很久都沒上來寫文章了,對于大多數人來說這里更像一個發現機會的平臺,在我這更像一個私密的小窩,說一些不足為人道的話。...
    Mocha_young閱讀 222評論 0 0
  • vjjjjhvccfvvbb
    大王大閱讀 330評論 0 0