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仍然是父類AB的創建過程與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

3738行的查找過程是類似的,只不過換成了函數而已。

子類對象操作自己的變量

41行:objB:myadd(10)

查找myadd函數的過程與查找obj.a的過程是一樣的,這裏再嘮叨一遍:

  1. objB 中並沒有函數 myadd,但是 objB 的元表是 B,而且 B.__index = B,所以就到 B 中去查找 myadd;

  2. 雖然 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設置鍵值對的時候:

  1. 如果鍵已經存在了,那麼就直接設置該鍵的值;

  2. 如果鍵不存在,那麼 lua 會看它的元表中是否有 __newindex 字段 (可以是一個 table,也可以是一個函數);

2-1. 如果有   __newindex 字段,那麼就是調用 __newindex (如果是一個函數),或者在 __newindex 中添加鍵值對 (如果是一個 table);

2-2. 如果沒有 __newindex 字段,那麼就直接在 objB 中存儲該鍵值對;

根據上面這個規則,就會設置objB.a = 11

明白以上這些之後,4243行的打印語句就不復雜了。

42行:objA最終找到的a是父類A中的成員a,打印出:objA.a = 1

43行:objB中自己已經有了成員a,所以打印出:objB.a = 11

繼續往下繼承

有了上面的基礎,再從子類B中派生出類CC派生出類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