UH2S1L12——Lua的面向对象
UH2S1L12——Lua的面向对象
Lua的面向对象
Lua没有原生的面向对象实现,因此我们必须借助表和元表自行实现面向对象
我们不仅要实现类(包括成员变量与方法,还要构造方法),还要实现面向对象有三大特性,封装、继承、多态
本章实现
1 | -- 万物之父Object,所有的类都继承自它,提供最基础的继承与实例化方法 |
封装
实现目标:可以通过类实例化对象,并可以调用对象的成员变量与方法
将成员变量和方法放入表内,通过表调用这些变量与方法,就已经有了封装
但由于没有实例化对象,导致我们声明出来的“类”几乎不能复用,因此我们需要自己实现一个实例化方法
值得一提的是,无论是类的声明,还是通过类实例化的对象,它们都是表,实例化对象就是创建一个自动拥有类的成员变量和方法的表
下面会将:用来声明类的成员的表记为类的声明表,作为实例化出来的对象的表称为类对象表
首先声明一个万物之父Object
类,并向其中添加所有类都必须要的实例化方法,用来实例化对象
实例化方法实现思路:
-
function Object:new()
:在万物之父Object
类声明一个所有类都会用的实例化方法,
这里:
就是默认声明一个参数self
,因为类在实例化对象时,都需要把 类的声明表 本身传入进来,以供下面的逻辑使用 -
local obj = {}
:在Lua里,实例化一个对象本质上也是返回一个新的表给外部 -
self.__index = self
:将类的声明表的__index
设置为自己,使外部在类对象表找不到某一个索引时,会到类的声明表里去找对象的成员变量重新赋值前,类对象表实际不存在这个成员变量索引,这样设置可以让外部在重新赋值前仍然可以调用对象的成员
对象的成员变量重新赋值后,类对象表会真正拥有该成员变量索引和值,这时再调用对象的成员变量就是调用类对象表里的值 -
setmetatable(obj, self)
:将 类的声明表 作为 类对象表 的元表,
1 | Object = {} |
这样,调用Object:new
方法后,就会实例化一个Object
对象表(对象可以调用Object
的所有成员方法与变量)
假设Object
类存在id
和Test
成员,实例化一个对象出来,为其成员变量赋值,检查赋值前后成员的调用,以及赋值是否会影响原来的Object
表
1 | print("**********面向对象************") |
1 | **********面向对象************ |
可见:
- 一开始未赋值时
myObj.id
实际调用的是用于声明Object
类的表内的id
,赋值后调用的就是myObj
表内自己的id
- 虽然
myObj:Test()
调用的实际上是用于声明Object
类的表内的Test
方法,
但由于调用者是myObj
且使用:
调用,这使得Test
方法实际上传入的是myObj
自己,因此Test
方法内调用的也是myObj.id
实践可见,这样实现的封装和实例化对象都没有问题
继承
实现目标:类声明时需要继承另外一个类,子类实例化出来的对象可以调用父类的成员变量与方法
前置知识:
_G
表是存储所有全局变量的表,因此为表内添加键与值也算添加全局变量,使用键调用_G
表内的值也算调用全局变量
这样我们可以通过字符串来添加或者调用全局变量
实现思路:
-
function Object:subClass(className)
:在万物之父Object
类声明一个所有类都会用的subClass
继承方法,
调用方法是:父类名:subclass(子类名)
,它的作用可以类比为C#的public class 子类名 : 父类名 { }
这里:
就是默认声明一个参数self
,因为父类在派生出新子类时,需要把作为父类的表自己传入进来,供下面的逻辑使用
同时还要传入一个子类名,向_G
表添加键值对,子类名为键,空表为值,这样就可以创建子类的声明表的全局变量 -
_G[className] = {}
:相当于声明了名字为className
的全局变量,赋值为空表,该空表就是子类的声明表,可通过这个全局变量调用 -
self.__index = self
:将父类的声明表的__index
设置为自己,当子类的声明表找不到某个索引时,回到父类的声明表查找该索引
这是通过子类调用父类成员变量和方法的关键步骤,因为 子类对象表 和 子类的声明表 在重新赋值前都没有真正的 存储父类的声明表的成员变量索引 -
setmetatable(obj, self)
:将 父类的声明表 作为 子类的声明表 的元表
1 | function Object:subClass(className) |
这样我们就实现了声明类的方法,它必须要继承另外一个类,至少要继承万物之父Object
,通过它声明的类实例化出来的对象可以继承父类的所有成员
使用该继承方法即可声明一个新的子类,子类会继承父类的所有成员变量与方法
1 | Object:subClass("Person") |
1 | 1 |
可见,虽然new
方法存储在Object
声明表内,但是Person
的声明表依然可以调用它来实例化对象
同时,子类实例化出来的p1
依然可以调用父类Object
声明的成员变量和方法,
即使p1
表内没有存储Object
声明的成员变量和方法,可见确实实现了继承的特性
接下来Person再声明一些成员变量,然后继承Person声明两个类并实例化对象做测试
1 | Object:subClass("Person") |
1 | true |
可以看到两个类都继承了父类的成员变量和方法,实例化出来的对象也没有问题
多态
实现目标:继承父类的子类,拥有和相同的父类的成员方法,但是两者执行逻辑不同,
也就是子类可以重写继承自父类的成员方法(还需要可以保留父类的逻辑)
重写方法其实非常简单,只需要成员变量重新赋值一样,将成员方法重新赋值一个新的成员方法即可
这样新的成员方法就会存放在新的子类表内,父类声明的成员变量的初始值也这样可以修改
1 | Object:subClass("GameObject") |
1 | table: 00B39B20: 重写 |
但是,单纯只是这样重写,我们不能像C#使用base.function()
保留原来父类实现的逻辑
我们可以在继承的方法里为子类声明一个新的成员变量base
并赋值父类,这样可以让我们通过base
来调用父类的方法
1 | function Object:subClass(className) |
1 | Object:subClass("GameObject") |
1 | 玩家移动到(1, 1) |
可以看见在修改了继承方法后,我们就可以通过self.base:function()
保留父类的逻辑了
但是! self.base:function()
这一句存在严重的问题!
1 | print("**********多态************") |
1 | 玩家移动到(1, 1) |
调用不同对象的Move
方法居然出现了不同对象共用成员变量的情况,这显然违背了面向对象
这是因为self.base:function()
等价于self.base.function(base)
,而base
指向的是父类GameObject
的声明表
这导致Move
方法实际上传入了父类GameObject
的声明表,并在此基础上修改了父类GameObject
的声明表的成员变量值
而实例化出来的子类对象表实际上没有父类GameObject
的声明表的成员变量索引,
由于我们实现的封装逻辑导致的,这时直接调用子类对象成员变量,实际会调用父类GameObject
的声明表的成员变量
而父类GameObject
的声明表的成员变量值又被修改了,这就出现了不同对象共用成员变量的现象
解决方法很简单,只需要修改调用父类方法传入的参数即可,
也就是将self.base:function()
改为self.base.function(self)
1 | print("**********多态************") |
1 | 玩家table: 00AE9DF0移动到(1, 1) |
self.base.function(self)
是在调用父类的声明表的方法时,传入调用方法的表自己,而不是传入表的base
变量
这样,可以确保传入的是子类对象表,为传入的表的变量赋值时,不会修改父类声明的成员变量,而是赋值给子类对象表的变量
根据我们实现的封装的逻辑,子类对象的成员变量重新赋值后,子类对象表会真正的存储成员变量,外部再调用也不会调用父类的声明表的成员变量
笔者私货
就直说吧,语法残废的Lua写面向对象就是依托答辩,用它处处都是坑,还要跑到那才知道有问题,
元表没写好,:
或者.
搞混了,怎么死的都不知道,一点一点调试吖史去吧,把精力浪费在这种东西上简直是对人生的浪费啊!!!
用这玩意写游戏这种刚需面向对象的项目太痛苦了,赶紧来个热更新新技术彻底替代掉这托构思玩意吧,球球了!!!