UH2S1L12——Lua的面向对象

Lua的面向对象

Lua没有原生的面向对象实现,因此我们必须借助表和元表自行实现面向对象
我们不仅要实现类(包括成员变量与方法,还要构造方法),还要实现面向对象有三大特性,封装、继承、多态

本章实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 万物之父Object,所有的类都继承自它,提供最基础的继承与实例化方法
Object = {}

-- 类的实例化方法,返回实例化的对象
function Object:new()
local obj = {}
self.__index = self
setmetatable(obj, self)
return obj
end

-- 声明新的类的方法,需要继承哪个类就让哪个类执行该方法,声明出来的类会自动声明base成员变量,指向其父类
function Object:subClass(className)
_G[className] = {}
local newClass = _G[className]
self.__index = self
setmetatable(newClass, self)
newClass.base = self
end

封装

实现目标:可以通过类实例化对象,并可以调用对象的成员变量与方法

将成员变量和方法放入表内,通过表调用这些变量与方法,就已经有了封装
但由于没有实例化对象,导致我们声明出来的“类”几乎不能复用,因此我们需要自己实现一个实例化方法

值得一提的是,无论是类的声明,还是通过类实例化的对象,它们都是表,实例化对象就是创建一个自动拥有类的成员变量和方法的表
下面会将:用来声明类的成员的表记为类的声明表,作为实例化出来的对象的表称为类对象表

首先声明一个万物之父Object,并向其中添加所有类都必须要的实例化方法,用来实例化对象

实例化方法实现思路:

  1. function Object:new()​:在万物之父Object​类声明一个所有类都会用的实例化方法,
    这里:​就是默认声明一个参数self​,因为类在实例化对象时,都需要把 类的声明表 本身传入进来,以供下面的逻辑使用

  2. local obj = {}​:在Lua里,实例化一个对象本质上也是返回一个新的表给外部

  3. self.__index = self​:将类的声明表的__index​设置为自己,使外部在类对象表找不到某一个索引时,会到类的声明表里去找

    对象的成员变量重新赋值前,类对象表实际不存在这个成员变量索引,这样设置可以让外部在重新赋值前仍然可以调用对象的成员
    对象的成员变量重新赋值后,类对象表会真正拥有该成员变量索引和值,这时再调用对象的成员变量就是调用类对象表里的值

  4. setmetatable(obj, self)​:将 类的声明表 作为 类对象表 的元表,

1
2
3
4
5
6
7
8
Object = {}

function Object:new()
local obj = {}
self.__index = self
setmetatable(obj, self)
return obj
end

这样,调用Object:new​方法后,就会实例化一个Object​对象表(对象可以调用Object​的所有成员方法与变量)

假设Object​类存在id​和Test​成员,实例化一个对象出来,为其成员变量赋值,检查赋值前后成员的调用,以及赋值是否会影响原来的Object​表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
print("**********面向对象************")

Object = {}

function Object:new()
local obj = {}
self.__index = self
setmetatable(obj, self)
return obj
end

Object.id = 1
function Object:Test()
print(self.id)
end

local myObj = Object:new()
print(Object)
print(myObj)
print(myObj.id)

myObj:Test()

myObj.id = 2
myObj:Test()

print(Object.id)
Object:Test()
1
2
3
4
5
6
7
8
**********面向对象************
table: 00A89BC0
table: 00A89918 --两者不同,说明声明出来的对象表和作为类的声明的表是不同的,说明确实实例化了对象
1 --虽然没有初始化,但是实例化出来的myObj调用成员变量与方法没有出现问题
1
2 --赋值后,Test()的输出确实改变了,即使Test()实际存储在Object表内,但由于调用者是myObj且使用:调用,因此调用的是myObj.id
1 --输出Object.id和执行Object:Test(),确认修改myObj值不会影响原来类声明的值
1

可见:

  • 一开始未赋值时myObj.id​实际调用的是用于声明Object​类的表内的id​,赋值后调用的就是myObj​表内自己的id
  • 虽然myObj:Test()​调用的实际上是用于声明Object​类的表内的Test​方法,
    但由于调用者是myObj​且使用:​调用,这使得Test​方法实际上传入的是myObj​自己,因此Test​方法内调用的也是myObj.id

实践可见,这样实现的封装和实例化对象都没有问题

继承

实现目标:类声明时需要继承另外一个类,子类实例化出来的对象可以调用父类的成员变量与方法

前置知识:
_G​表是存储所有全局变量的表,因此为表内添加键与值也算添加全局变量,使用键调用_G​表内的值也算调用全局变量
这样我们可以通过字符串来添加或者调用全局变量

实现思路:

  1. function Object:subClass(className)​:在万物之父Object​类声明一个所有类都会用的subClass​继承方法,
    调用方法是:父类名:subclass(子类名)​,它的作用可以类比为C#的public class 子类名 : 父类名 { }
    这里:​就是默认声明一个参数self​,因为父类在派生出新子类时,需要把作为父类的表自己传入进来,供下面的逻辑使用
    同时还要传入一个子类名,向_G​表添加键值对,子类名为键,空表为值,这样就可以创建子类的声明表的全局变量
  2. _G[className] = {}​:相当于声明了名字为className​的全局变量,赋值为空表,该空表就是子类的声明表,可通过这个全局变量调用
  3. self.__index = self​:将父类的声明表的__index​设置为自己,当子类的声明表找不到某个索引时,回到父类的声明表查找该索引
    这是通过子类调用父类成员变量和方法的关键步骤,因为 子类对象表 和 子类的声明表 在重新赋值前都没有真正的 存储父类的声明表的成员变量索引
  4. setmetatable(obj, self)​:将 父类的声明表 作为 子类的声明表 的元表
1
2
3
4
5
6
function Object:subClass(className)
_G[className] = {}
local newClass = _G[className]
self.__index = self
setmetatable(newClass, self)
end

这样我们就实现了声明类的方法,它必须要继承另外一个类,至少要继承万物之父Object​,通过它声明的类实例化出来的对象可以继承父类的所有成员

使用该继承方法即可声明一个新的子类,子类会继承父类的所有成员变量与方法

1
2
3
4
5
Object:subClass("Person")
local p1 = Person:new()
print(p1.id)
p1.id = 100
p1:Test()
1
2
1
100

可见,虽然new​方法存储在Object​声明表内,但是Person​的声明表依然可以调用它来实例化对象
同时,子类实例化出来的p1​依然可以调用父类Object​声明的成员变量和方法,
即使p1​表内没有存储Object​声明的成员变量和方法,可见确实实现了继承的特性

接下来Person再声明一些成员变量,然后继承Person声明两个类并实例化对象做测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Object:subClass("Person")
-- 声明Person的成员变量
Person.age = 0
Person.sex = false
-- 声明Person的成员方法
function Person:Speak()
print("你好,我今年" .. self.age .. "岁")
end

-- 声明两个继承Person的类
Person:subClass("Player")
Person:subClass("Monster")

-- 调用继承自Object的实例化方法
local p1 = Player:new()
local m1 = Monster:new()

-- 调用继承自父类的成员变量
p1.id = 100
p1.age = 20
p1.sex = true

m1.id = 200
m1.age = 30
m1.sex = false

print(p1.sex)
print(m1.sex)

-- 调用继承自父类的成员方法
p1:Test()
m1:Test()
p1:Speak()
m1:Speak()
1
2
3
4
5
6
true
false
100
200
你好,我今年20
你好,我今年30

可以看到两个类都继承了父类的成员变量和方法,实例化出来的对象也没有问题

多态

实现目标:继承父类的子类,拥有和相同的父类的成员方法,但是两者执行逻辑不同,
也就是子类可以重写继承自父类的成员方法(还需要可以保留父类的逻辑)

重写方法其实非常简单,只需要成员变量重新赋值一样,将成员方法重新赋值一个新的成员方法即可
这样新的成员方法就会存放在新的子类表内,父类声明的成员变量的初始值也这样可以修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Object:subClass("GameObject")
GameObject.posX = 0;
GameObject.posY = 0;
GameObject.id = 1000;
function GameObject:Move()
self.posX = self.posX + 1
self.posY = self.posY + 1
print(string.format("玩家移动到(%d, %d)", self.posX, self.posY))
end

GameObject:subClass("Player")
function Player:Move()
print(tostring(self) .. ": 重写")
end

local p1 = Player:new()
p1:Move()
p1:Test()
1
2
table: 00B39B20: 重写
1000

但是,单纯只是这样重写,我们不能像C#使用base.function()​保留原来父类实现的逻辑

我们可以在继承的方法里为子类声明一个新的成员变量base​并赋值父类,这样可以让我们通过base​来调用父类的方法

1
2
3
4
5
6
7
function Object:subClass(className)
_G[className] = {}
local newClass = _G[className]
self.__index = self
newClass.base = self --这一句使得子类可以调用自己的父类,进而调用父类的成员方法
setmetatable(newClass, self)
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Object:subClass("GameObject")
GameObject.posX = 0;
GameObject.posY = 0;
GameObject.id = 1000;
function GameObject:Move()
self.posX = self.posX + 1
self.posY = self.posY + 1
print(string.format("玩家移动到(%d, %d)", self.posX, self.posY))
end

GameObject:subClass("Player")
function Player:Move()
self.base:Move() --利用base来调用父类的Move方法,保留父类的逻辑
print(tostring(self) .. ": 重写")
end

local p1 = Player:new()
p1:Move()
1
2
玩家移动到(1, 1)
table: 00C197D8: 重写

可以看见在修改了继承方法后,我们就可以通过self.base:function()​保留父类的逻辑了
但是! self.base:function() 这一句存在严重的问题!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
print("**********多态************")
Object:subClass("GameObject")
GameObject.posX = 0;
GameObject.posY = 0;
GameObject.id = 1000;
function GameObject:Move()
self.posX = self.posX + 1
self.posY = self.posY + 1
print(string.format("玩家移动到(%d, %d)", self.posX, self.posY))
end

GameObject:subClass("Player")
function Player:Move()
self.base:Move()
print(tostring(self) .. ": 重写")
end

local p1 = Player:new()
local p2 = Player:new()

p1:Move()
p2:Move()
p1:Move()
1
2
3
4
5
6
玩家移动到(1, 1)
table: 006F9850: 重写
玩家移动到(2, 2)
table: 006F98F0: 重写
玩家移动到(3, 3)
table: 006F9850: 重写

调用不同对象的Move​方法居然出现了不同对象共用成员变量的情况,这显然违背了面向对象

这是因为self.base:function()​等价于self.base.function(base)​,而base​指向的是父类GameObject​的声明表
这导致Move​方法实际上传入了父类GameObject​的声明表,并在此基础上修改了父类GameObject​的声明表的成员变量值
而实例化出来的子类对象表实际上没有父类GameObject​的声明表的成员变量索引,
由于我们实现的封装逻辑导致的,这时直接调用子类对象成员变量,实际会调用父类GameObject​的声明表的成员变量
而父类GameObject​的声明表的成员变量值又被修改了,这就出现了不同对象共用成员变量的现象

解决方法很简单,只需要修改调用父类方法传入的参数即可,
也就是将self.base:function()​改为self.base.function(self)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
print("**********多态************")
Object:subClass("GameObject")
GameObject.posX = 0;
GameObject.posY = 0;
GameObject.id = 1000;
function GameObject:Move()
self.posX = self.posX + 1
self.posY = self.posY + 1
print(string.format("玩家%s移动到(%d, %d)", tostring(self), self.posX, self.posY))
end

GameObject:subClass("Player")
function Player:Move()
self.base.Move(self)
print(tostring(self) .. ": 重写")
end

local p1 = Player:new()
local p2 = Player:new()

p1:Move()
p2:Move()
p1:Move()
1
2
3
4
5
6
玩家table: 00AE9DF0移动到(1, 1)
table: 00AE9DF0: 重写
玩家table: 00AE9E40移动到(1, 1)
table: 00AE9E40: 重写
玩家table: 00AE9DF0移动到(2, 2)
table: 00AE9DF0: 重写

self.base.function(self)​是在调用父类的声明表的方法时,传入调用方法的表自己,而不是传入表的base​变量
这样,可以确保传入的是子类对象表,为传入的表的变量赋值时,不会修改父类声明的成员变量,而是赋值给子类对象表的变量
根据我们实现的封装的逻辑,子类对象的成员变量重新赋值后,子类对象表会真正的存储成员变量,外部再调用也不会调用父类的声明表的成员变量

笔者私货

就直说吧,语法残废的Lua写面向对象就是依托答辩,用它处处都是坑,还要跑到那才知道有问题,
元表没写好,:​或者.​搞混了,怎么死的都不知道,一点一点调试吖史去吧,把精力浪费在这种东西上简直是对人生的浪费啊!!!
用这玩意写游戏这种刚需面向对象的项目太痛苦了,赶紧来个热更新新技术彻底替代掉这托构思玩意吧,球球了!!!