7.[重要] lua面向对象*

lua的面向对象、类等概念其实都是基于table和metatable实现的

完整代码**

--面向对象实现 
--万物之父 所有对象的基类 Object
--封装
Object = {}
--实例化方法
function Object:new()
    --一定要使用local,否则就变成全局变量了
    local obj = {}
    --给空对象设置元表 以及 __index
    self.__index = self
    setmetatable(obj, self)
    return obj
end
--继承
function Object:subClass(className)
    --根据名字生成一张表 就是一个类
    _G[className] = {}
    local obj = _G[className]
    --设置自己的“父类”,以实现多态
    obj.base = self
    --给子类设置元表 以及 __index
    self.__index = self
    setmetatable(obj, self)
end

--申明一个新的类
Object:subClass("GameObject")
--成员变量
GameObject.posX = 0
GameObject.posY = 0
--成员方法
function GameObject:Move()
    self.posX = self.posX + 1
    self.posY = self.posY + 1
end

--实例化对象使用
local obj = GameObject:new()
print(obj.posX)
obj:Move()
print(obj.posX)

local obj2 = GameObject:new()
print(obj2.posX)
obj2:Move()
print(obj2.posX)

--申明一个新的类 Player 继承 GameObject
GameObject:subClass("Player")
--多态 重写了 GameObject的Move方法
function Player:Move()
    --base调用父类方法 用. 自己传第一个参数
    self.base.Move(self)
end
print("****")
--实例化Player对象
local p1 = Player:new()
--使用的Gameobject的posX
print(p1.posX)
p1:Move()
--赋值之后调用的是p1自己的posX
print(p1.posX)

local p2 = Player:new()
print(p2.posX)
p2:Move()
print(p2.posX)

输出:

0
1
0
1
****
0
1
0
1

封装

image

  • 当使用:调用方法时,self代表调用者(默认把调用者传入)
  • 本质上声明一个“对象”,就是声明一个空表
print("**********封装************")
--面向对象 类 其实都是基于 table来实现
--元表相关的知识点
Object = {}
Object.id = 1

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

--冒号 是会自动将调用这个函数的对象 作为第一个参数传入的写法
function Object:new()
    --self 代表的是 我们默认传入的第一个参数
    --对象就是变量 返回一个新的变量
    --返回出去的内容 本质上就是表对象
    local obj = {}
    --元表知识 __index 当找自己的变量 找不到时 就会去找元表当中__index指向的内容
    self.__index = self
    setmetatable(obj, self)
    return obj
end

注意!!!!

local myObj = Object:new()
print(myObj)
-- 因为找不到myObj的id,但由于设置__idex,所以会输出Object的id 1
print(myObj.id)
myObj:Test()
-- 对空表中 申明一个新的属性 叫做id(没有设置__newIndex,会设置到本身)
myObj.id = 2
-- 默认吧myObj传入,所以输出2
myObj:Test()

输出:

table: 0059A5A0
1
1
2

继承

image


--面向对象 类 其实都是基于 table来实现
--元表相关的知识点
Object = {}
Object.id = 1

function Object:Test()
    print(self.id)
end
--冒号 是会自动将调用这个函数的对象 作为第一个参数传入的写法
function Object:new()
    --self 代表的是 我们默认传入的第一个参数
    --对象就是变量 返回一个新的变量
    --返回出去的内容 本质上就是表对象
    local obj = {}
    --元表知识 __index 当找自己的变量 找不到时 就会去找元表当中__index指向的内容
    self.__index = self
    setmetatable(obj, self)
    return obj
end
print("**********继承************")
--C# class 类名 : 继承类
--写一个用于继承的方法
function Object:subClass(className)
    -- _G知识点 是总表 所有声明的全局标量 都以键值对的形式存在其中
    _G[className] = {}
    --写相关继承的规则
    --用到元表
    local obj = _G[className]
    self.__index = self
    --子类 定义个base属性 base属性代表父类
    obj.base = self
    setmetatable(obj, self)
end

--print(_G)
--_G["a"] = 1
--_G.b = "123"
--print(a)
--print(b)

Object:subClass("Person")

local p1 = Person:new()
print(p1.id)
p1.id = 100
print(p1.id)
p1:Test()

Object:subClass("Monster")
local m1 = Monster:new()
print(m1.id)
m1.id = 200
print(m1.id)
m1:Test()

输出:

1
100
100
1
200
200

注意!!!*

Object:subClass("Person")

local p1 = Person:new()
--输出1 Object的id
print(p1.id)

输出的是Object的id

因为:

  1. p1调用Person:new(),会将person设置为自己的元表,并设置__index指向Person
  2. 而Person又调用Object:SubClass(),将Object设置为自己的元表 并设置__index指向Object

即 p1 -> Person -> Object

再根据__index会持续向上寻找最后输出1

多态

image

Object = {}
Object.id = 1

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

--冒号 是会自动将调用这个函数的对象 作为第一个参数传入的写法
function Object:new()
    --self 代表的是 我们默认传入的第一个参数
    --对象就是变量 返回一个新的变量
    --返回出去的内容 本质上就是表对象
    local obj = {}
    --元表知识 __index 当找自己的变量 找不到时 就会去找元表当中__index指向的内容
    self.__index = self
    setmetatable(obj, self)
    return obj
end
--C# class 类名 : 继承类
--写一个用于继承的方法
function Object:subClass(className)
    -- _G知识点 是总表 所有声明的全局标量 都以键值对的形式存在其中
    _G[className] = {}
    --写相关继承的规则
    --用到元表
    local obj = _G[className]
    self.__index = self
    --子类 定义个base属性 base属性代表父类
    obj.base = self
    setmetatable(obj, self)
end
print("**********多态************")
--相同行为 不同表象 就是多态
--相同方法 不同执行逻辑 就是多态
Object:subClass("GameObject")
GameObject.posX = 0;
GameObject.posY = 0;
function GameObject:Move()
    self.posX = self.posX + 1
    self.posY = self.posY + 1
    print(self.posX)
    print(self.posY)
end

GameObject:subClass("Player")
function Player:Move()
    --base 指的是 GameObject 表(类)
    --这种方式调用 相当于是把基类表 作为第一个参数传入了方法中
    --避免把基类表 传入到方法中 这样相当于就是公用一张表的属性了
    --我们如果要执行父类逻辑 我们不要直接使用冒号调用
    --要通过.调用 然后自己传入第一个参数 
    self.base.Move(self)
end

local p1 = Player:new()
p1:Move()
p1:Move()
--目前这种写法 有坑 不同对象使用的成员变量 居然是相同的成员变量
--不是自己的
local p2 = Player:new()
p2:Move()

输出:


1
1
2
2
1
1

注意

如何实现子类调用父类的方法

function Object:subClass(className)
    -- _G知识点 是总表 所有声明的全局标量 都以键值对的形式存在其中
    _G[className] = {}
    --写相关继承的规则
    --用到元表
    local obj = _G[className]
    self.__index = self
    --子类 定义个base属性 base属性代表父类
    obj.base = self
    setmetatable(obj, self)
end

继承方法增加一个base,给新的类(表)设置一个base指向self(因为调用是采用Object:subClass,此时的self是Object)

坑!!!!!**

对于

Object:subClass("GameObject")
GameObject.posX = 0;
GameObject.posY = 0;
function GameObject:Move()
    self.posX = self.posX + 1
    self.posY = self.posY + 1
    print(self.posX)
    print(self.posY)
end
GameObject:subClass("Player")
function Player:Move()
    --base 指的是 GameObject 表(类)
    --这种方式调用 相当于是把基类表 作为第一个参数传入了方法中
    --避免把基类表 传入到方法中 这样相当于就是公用一张表的属性了
    --我们如果要执行父类逻辑 我们不要直接使用冒号调用
    --要通过.调用 然后自己传入第一个参数 
    self.base:Move()
end

如果我们使用self.base:Move(self)

此时会将base默认传入base(父类)

此时如果我们

local p1 = Player:new()
p1:Move()
--目前这种写法 有坑 不同对象使用的成员变量 居然是相同的成员变量
--不是自己的
local p2 = Player:new()
p2:Move()

我们会发现输出是

1
1
2
2

p1和p2共用父类GameObject的posX和posY,这显然是不向对象的

因为我们每次调用base:Move​都会默认吧GameObject传入,所以两次操作的都是GameObject这张表的posx和posy

为避免这个问题,当我们想要在子类使用父类方法的时候要使用self.base.Move(self)

GameObject:subClass("Player")
function Player:Move()
    --base 指的是 GameObject 表(类)
    --这种方式调用 相当于是把基类表 作为第一个参数传入了方法中
    --避免把基类表 传入到方法中 这样相当于就是公用一张表的属性了
    --我们如果要执行父类逻辑 我们不要直接使用冒号调用
    --要通过.调用 然后自己传入第一个参数 
    self.base.Move(self)
end

当我们调用

local p1 = Player:new()
p1:Move()
p1:Move()
--目前这种写法 有坑 不同对象使用的成员变量 居然是相同的成员变量
--不是自己的
local p2 = Player:new()
p2:Move()

此时Player:Move的self分别是p1和p2

那么我们操作的posX和posY就分别是p1和p2自己的posX和posY,此时输出

1
1
2
2
1
1

为什么会这样呢采用self.base.Move(self)就可以分别使用两个表的posX和posY呢?

因为在Move方法

--伪代码
function GameObject:Move()
    self.posX = self.posX + 1
    self.posY = self.posY + 1
end
p1:Move()

此时我们传入的self其实是表p1,此时p1是没有posX和posY的,调用self.posX的时候实际上使用的是GameObject的posX,得出结果1,此时赋值

相当于p1.posX=1相当于给p1新添加了一个元素posx=1

这篇文章详细介绍了如何在Lua中实现面向对象编程(OOP),主要基于table和metatable机制。文章分为封装和继承两部分:

  1. 封装:通过元表__index实现属性查找,使用冒号语法自动传递self参数,展示了如何创建类和实例化对象。
  2. 继承:利用_G全局表和元表机制实现子类继承父类功能,包括方法重写(多态)和通过base属性调用父类方法。
    完整代码示例演示了从基类Object派生子类GameObjectPlayer的过程,包括成员变量/方法的定义、实例化及多态调用。输出结果验证了不同实例间的数据独立性。
最后修改:2025 年 06 月 05 日
如果觉得我的文章对你有用,请随意赞赏