程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> Lua中的面向對象編程

Lua中的面向對象編程

編輯:關於C語言
 

簡單說說Lua中的面向對象

Lua中的table就是一種對象,看以下一段簡單的代碼:
local tb1 = {a = 1, b = 2}
local tb2 = {a = 1, b = 2}
local tb3 = tb1

if tb1 == tb2 then
print("tb1 == tb2")
else
print("tb1 ~= tb2")
end

tb3.a = 3
print(tb1.a)

上述代碼會輸出tb1 ~= tb2。說明兩個具有相同值得對象是兩個不同的對象,同時在Lua中table是引用類型的。我在《Lua中的模塊與包》中也總結了,我們是基於table來實現的模塊,在table中可以定義函數,也就是說,每個table對象都可以擁有其自己的操作。看一段代碼:
Account = {balance = 0}
function Account.withDraw(v)
Account.balance = Account.balance - v
end

Account.withDraw(10) -- 調用函數
print(Account.balance)

上面的代碼創建了一個新函數,並將該函數存入Account對象的withDraw字段中,然後我們就可以調用該函數了。不過,在函數中使用全局名稱Account是一個不好的編程習慣,因為這個函數只能針對特定對象工作,並且,這個特定對象還必須存儲在特定的全局變量中。如果改變了對象的名稱,withDraw就再也不能工作了。例如以下代碼:
a = Account
Account = nil
a.withDraw(100)

這樣就會出現錯誤。我在這裡使用Account創建了一個新的對象a,當將Account賦值為nil時,應該要對a對象不產生任何影響。但是,由於在函數withDraw內部使用了Account,而不是變量a,所以就出現了錯誤。如果我們將withDraw函數內部的Account.balance = Account.balance – v語句修改為:a.balance = a.balance – v,這樣就不會出現錯誤了。這就表明,當我們需要對一個函數進行操作時,需要指定實際的操作對象,即這裡的a,這就需要一個額外的參數來表示該操作者,就好比C++中的this一樣,只不過這裡將這個關鍵字換成了self,換完以後的代碼如下:
Account = {balance = 0}
function Account.withDraw(self, v)
self.balance = self.balance - v
end

a = Account
Account = nil
a.withDraw(a, 100)
print(a.balance)

這樣再調用,就不會出現錯誤了。

使用self參數是所有面向對象語言的一個核心。大多數面向對象語言都對程序員隱藏了self參數,從而使得程序員不必顯示地聲明這個參數。Lua也可以,當我們在定義函數時,使用了冒號,則能隱藏該參數,那麼上述代碼使用冒號來改下,就是下面這個樣子了。
Account = {balance = 0}
function Account:withDraw(v) -- 注意這裡的冒號":"
self.balance = self.balance - v
end

a = Account
Account = nil
a:withDraw(100) -- 注意這裡的調用時,也需要冒號":"
print(a.balance)

冒號的作用很簡單,就是在方法定義中添加一個額外的隱藏參數,以及在一個方法調用中添加一個額外的實參。冒號只是一種語法便利,並沒有引入任何新的東西;如果你願意,你可以可以不使用self,而是在每次定義一個函數時,手動的加上self,只要你處理好了self,它們都是一樣的。

這裡亂亂的講了一些Lua中的東西,主要還是說了table是一個不一樣的東西,還有self。接下來,就正式進入面向對象的世界。不要忘了,上面總結的東西是非常有用的。

類是什麼?一個類就是一個創建對象的模具。例如C++中,每個對象都是某個特定類的實例。在C++中,如果一個類沒有進行實例化,那這個類中對應的操作,基本就是一堆“沒有用”的代碼;而Lua則不一樣,即使你不實例化一個“類”,你照樣也可以使用“類”名直接調用它的方法(對於C++,請忽視靜態的方法);這說明Lua中的“類”的概念與C++這種高級語言中類的概念還是有差別的。在Lua中則沒有類的概念,而我們都是通過Lua現有的支持,去模擬類的概念。在Lua中,要表示一個類,只需創建一個專用作其他對象的原型(prototype)。原型也是一種常規的對象,也就是說我們可以直接通過原型去調用對應的方法。當其它對象(類的實例)遇到一個未知操作時,原型會先查找它。

在Lua中實現原型是非常簡單的,比如有兩個對象a和b,要讓b作為a的原型,只需要以下代碼就可以完成:
setmetatable(a, {__index = b}) -- 又是元表,不會的請看前幾篇關於元表的文章

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

好了,現在有了原型,那如何使用這個原型創建一個“實例”呢?接著看以下代碼:
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{value = 100} -- 這裡使用原型Account創建了一個對象a
a:display()

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

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

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

繼承不僅可以用於方法,還可以作用於字段。因此,一個類不僅可以提供方法,還可以為實例中的字段提供默認值。看以下代碼:
local Account = {value = 0}
function Account:new(o) -- 這裡是冒號哦
o = o or {} -- 如果用戶沒有提供table,則創建一個
setmetatable(o, self)
self.__index = self
return o
end

function Account:display()
self.value = self.value + 100
print(self.value)
end

local a = Account:new{} -- 這裡使用原型Account創建了一個對象a
a:display() --(1)
a:display() --(2)

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

在display的定義中,就會變成這樣子:
a.value = getmetatable(a).__index(value) + 100

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

繼承

由於類也是對象(准確地說是一個原型),它們也可以從其它類(原型)獲得(繼承)方法。這種行為就是繼承,可以很容易的在Lua中實現。現在我們有一個類(原型,其實在Lua中說類這個概念,還是很別扭的,畢竟用C++的腦袋去想,還是覺的有點奇怪的。)CA:
local CA = {value = 0}

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

function CA:display()
print(self.value)
end

function CA:addValue(v)
self.value = self.value + v
end

現在需要從這個CA類派生出一個子類CLittleA,則需要創建一個空的類,從基類繼承所有的操作:
local CLittleA = CA:new()

現在,我創建了一個CA類的一個實例對象,在Lua中,現在CLittleA既是CA類的一個實例對象,也是一個原型,就是所謂的類,就相當於CLittleA類繼承自CA類。再如下面的代碼:
local s = CLittleA:new{value1 = 10}

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

多重繼承

Lua中的面向對象編程
 

說到多重繼承,我在寫C++代碼的時候也用的很少,一般都是使用組合的方式解決的,對於“組合”這個概念不明白的朋友,可以閱讀我的設計模式系列的文章。既然說到了Lua中的多重繼承,那也總結一下,順便開拓一下視野和知識面。

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved