Lua 面向對象編程的基本原理示例
作 者:道哥,10 + 年嵌入式開發老兵,專注於:C/C++、嵌入式、Linux。
一些廢話
Lua
語言是一個小而美的語言,使用者不多。
估計閱讀這篇文章的人也不會多,姑且當做一篇筆記吧。
這篇文章主要描述:在Lua
語言中,如何通過table
結構來實現面向對象編程。
主要是看到某鳥教程上錯誤百出,估計示例代碼自己都沒有測試過;
關於Lua
語言中的table
以及metatable
的基本知識,這裏就不贅述了,官方手冊中描述的很清楚。
測試代碼
1 #!/usr/bin/lua
2
3 ------------------------------ class A
4 A = {
5 a = 1,
6 funcA = function()
7 print("this is funcA")
8 end
9 }
10
11 function A:new(t)
12 local t = t or {}
13 self.__index = self
14 setmetatable(t, self)
15 return t
16 end
17
18 function A:myadd(num)
19 self.a = self.a + num
20 end
21
22 objA = A:new()
23 print("objA.a = " .. objA.a)
24 print(objA.funcA())
25 print(string.rep("-", 20))
26
27 ------------------------------ class B
28 B = A:new({
29 b = 2,
30 funcB = function()
31 print("this is funcB")
32 end
33 })
34
35 objB = B:new()
36 print("objB.a = " .. objB.a)
37 print("objB.b = " .. objB.b)
38 print(objB.funcA())
39 print(objB.funcB())
40
41 objB:myadd(10)
42 print("objA.a = " .. objA.a)
43 print("objB.a = " .. objB.a)
執行結果如下:
$ ./oop.lua
objA.a = 1
this is funcA
--------------------
objB.a = 1
objB.b = 2
this is funcA
this is funcB
objA.a = 1
objB.a = 11
代碼說明
基類 (父類) A
首先來分析下4-25
行的代碼。
4-9
行:定義父類A
的成員變量和函數 (按照C++
中的習慣,可以叫做方法),可以看出Lua
語言中的函數是 “一等公民”,是可以賦值給一個變量的。
11-16
行:相當於是構造函數,用來創建一個父類A
的對象。
18-20
行:給父類A
增加一個函數,待會在分析子類B
的時候再說。
22
行:調用A:new()
函數,創建一個類A
的對象,賦值給變量objA
。
在A:new()
函數中,關鍵是第13
行代碼:此時self
等於A
,就相當於是A.__index = A
,這是合法的。
因爲函數的調用方式是A:new()
,Lua
的語法糖會把A
作爲第一個參數傳遞給new()
函數的第一個隱藏參數self
。
然後執行14
行的setmetatable(t, self)
,相當於把表t
的元表設置爲A
。
以上兩行搞明白之後,23-24
行的打印語句就簡單了:
23
行:因爲表objA
中沒有成員a
,但是objA
被設置了元表A
,而且該元表A
帶有__index
屬性,該屬性的值是表A
自己,於是就到A
中查找是否有成員a
,於是就打印出:
objA.a = 1
__index 屬性的值,可以是一個表,可以是一個函數;
只不過這裏特殊一點:__index 設置爲 A 自己;
24
行:查找函數的過程是一樣的,找到元表A
的__index
屬性的值,也就是表A
自己中的funcA
函數,然後調用,打印出:
this is funcA
派生類 (子類) B
28-33
行:定義了子類B
,其實它也是一個對象。
在創建函數A:new(t)
中,參數t
的值是:
local t = {
b = 2,
funcB = function()
print("this is funcB")
end
}
此時,self
仍然是父類A
,B
的創建過程與objA
的創建過程是一樣的,只不過給參數t
設置了子類B
自己的成員變量和函數。
所以,B
的元表被設置爲A
(14
行代碼的功勞),當然了A
的__index
仍然被設置爲A
自己。
關鍵是35
行:objB = B:new()
,得仔細嘮嘮。
子類B
並沒有自己的new
函數,但是類B
(也是一個 table
) 的元表被設置爲A
,並且A.__index = A
,所以最終就找到了A
中的new
函數,也就是11-16
行代碼。
進入這個函數中時,第一個隱藏參數self
被設置爲 B 了,因爲函數調用形式是:B:new()
。
所以:
13 行 self.__index = self 相當於設置 B.__index = B
14 行 etmetatable(t, self) 相當於把表 t 的元表設置爲 B
new()
函數返回之後,就把t
賦值給objB
。
下面再看一下36-39
行的打印語句:
36 print("objB.a = " .. objB.a)
37 print("objB.b = " .. objB.b)
38 print(objB.funcA())
39 print(objB.funcB())
36
行:objB
中並沒有成員a
,但是objB
的元表是B
,而且B.__index = B
,所以就到B
中去查找a
。
雖然B
中也沒有a
,但是B
的元表是A
,而且A.__index = A
,所以就在A
中找到了成員a
,打印出:
objB.a = 1
37
行:objB
中並沒有成員b
,但是objB
的元表是B
,而且B.__index = B
,所以在B
中找到了成員b
,因此打印出:
objB.b = 2
37
和38
行的查找過程是類似的,只不過換成了函數而已。
子類對象操作自己的變量
41
行:objB:myadd(10)
。
查找myadd
函數的過程與查找obj.a
的過程是一樣的,這裏再嘮叨一遍:
objB 中並沒有函數 myadd,但是 objB 的元表是 B,而且 B.__index = B,所以就到 B 中去查找 myadd;
雖然 B 中也沒有 myadd,但是 B 的元表是 A,而且 A.__index = A,所以就在 A 中找到了函數 myadd;
於是就調用了函數:
18 function A:myadd(num)
19 self.a = self.a + num
20 end
而且self
等於objB
,因此函數體中就等於是:
objB.a = objB.a + 10
加法表達式中的objB.a
的讀取過程,上面已經描述過了,最終定位到的是父類A
中的a
,即:1
。
1 + 10 = 11
,然後把11
賦值給objB.a
。
在賦值操作中,被賦值的objB.a
就不再是父類A
中的那個a
了!
因爲objB
本質是一個table
,給objB
設置鍵值對的時候:
如果鍵已經存在了,那麼就直接設置該鍵的值;
如果鍵不存在,那麼 lua 會看它的元表中是否有 __newindex 字段 (可以是一個 table,也可以是一個函數);
2-1. 如果有 __newindex 字段,那麼就是調用 __newindex (如果是一個函數),或者在 __newindex 中添加鍵值對 (如果是一個 table);
2-2. 如果沒有 __newindex 字段,那麼就直接在 objB 中存儲該鍵值對;
根據上面這個規則,就會設置objB.a = 11
。
明白以上這些之後,42
和43
行的打印語句就不復雜了。
42
行:objA
最終找到的a
是父類A
中的成員a
,打印出:objA.a = 1
。
43
行:objB
中自己已經有了成員a
,所以打印出:objB.a = 11
。
繼續往下繼承
有了上面的基礎,再從子類B
中派生出類C
,C
派生出類D
... 都不是什麼問題了,如下所示:
C = B:new()
objC = C:new()
print("objC.a = " .. objC.a)
print("objC.b = " .. objC.b)
print(objC.funcA())
print(objC.funcB())
感興趣的讀者可以自己測試一下。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/3n1Ak_9ashW14O1BRpEk7w