15.面向对象

15.面向对象


15.1 知识点

封装

  • Lua 中没有 C# 那种原生 class 语法。
  • Lua 里的面向对象,是用 table + function + metatable 模拟出来的。
  • 表保存字段和方法,元表负责查找规则,冒号调用负责把当前对象当成 self 传进去。

我们先学习怎么用表模拟对象,怎么 new 一个对象,怎么通过 __index 找到类表里的字段和方法。

创建万物之父类 Object表

Object = {}

给万物之父手动添加实例化的new方法

-- 使用到的知识:在类外使用 : 添加成员方法
-- Lua 里的类就是表,没有 C# 那种原生实例化语法
-- 所以这里给 Object 手动添加一个 new 方法

function Object:new()
    -- 实例化方法的目的,是返回一张新的对象表
    -- Lua 中的对象就是一张表

    local obj = {}          -- 要实例化的对象表,写成 local,避免变成全局变量

    setmetatable(obj, self) -- 设置当前类表为对象表的元表

    self.__index = self     -- 设置 __index 指向当前类表
                            -- 这样对象找不到字段或方法时,就会去类表中找

    return obj              -- 返回对象
end

给万物之父设置一个属性id

Object.id = 1

给万物之父设置打印id属性的方法

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

实例化对象并使用

local myObj = Object:new() -- 实例化对象

print(myObj)               -- table: 00BAA670,对象本质还是表

print(myObj.id)            -- 1
-- myObj 自己没有 id 属性
-- 所以会去 myObj 的元表 Object 的 __index 指向的 Object 中找 id

myObj:PrintId()            -- 1
-- myObj 自己没有 PrintId 方法
-- 也是去 Object 中找到 PrintId 方法
-- 使用 : 调用时,会把 myObj 作为第一个参数传进去,也就是方法里的 self

myObj.id = 2               -- 给当前对象设置一个自己的 id 属性

print(Object.id)           -- 1,Object 上的 id 没有被改动

print(myObj.id)            -- 2
-- myObj 自己已经有 id 了,就不会再去 Object 中找默认值

myObj:PrintId()            -- 2
-- PrintId 方法仍然来自 Object
-- 但 self 是 myObj,所以打印的是 myObj 自己的 id

这里要分清楚:方法通常放在类表上共用,实例自己的数据放在对象表上。
这样每个对象可以有自己的字段值,但方法不用每个对象都复制一份。

继承

回顾_G表

-- 继承这里用到了 _G 表
-- _G 也是一张表,以键值对的形式存储全局变量

print(_G) -- table: 00D54F00,_G 是一个表

-- 设置全局变量 a 的初始值为 111
_G["a"] = 111
print(a) -- 111

-- 修改全局变量 a 的值为 222
_G.a = 222
print(a) -- 222

-- 尝试将全局变量 a 的值作为 key,设置 _G 表中的值
-- 注意:这不会改变全局变量 a 本身
_G[a] = 333
print(a)       -- 222
print(_G[222]) -- 333

-- 修改全局变量 a 的值为字符串 "a"
_G.a = "a"
print(a)     -- a
print(_G[a]) -- a,这样写等同于 _G["a"],因为 a = "a"

给万物之父设置继承的方法 要传入子类名 要双引号包裹

function Object:subClass(className)
    -- 继承的目的,是设置子类表和父类表之间的查找关系
    -- 这里通过元表建立继承关系

    -- 在 _G 中用传入的类名创建一张全局表
    _G[className] = {}

    -- 使用元表建立继承规则
    _G[className].base = self         -- base 表示父类表,self 表示当前调用 subClass 的类
    setmetatable(_G[className], self) -- 设置子类的元表为当前父类
    self.__index = self               -- 父类的 __index 指向父类自己
                                       -- 这样子类找不到字段和方法时,可以去父类中找
end

子类继承万物之父Object

Object:subClass("Person")    -- 子类 Person 继承 Object

print(Person)                -- table: 00E5A4B8

local person1 = Person:new() -- 实例化 Person
-- Person 现在是空表,没有 new 方法
-- 去 Person 的元表 Object 的 __index 指向的 Object 中找,找到 new 方法
-- new 方法会把 Person 作为第一个参数传进去
-- Person 的 __index 会设置成 Person 自己
-- 返回一个实例化 Person 的对象,并且这个对象的元表是 Person

print(person1.id) -- 1
-- person1 自己没有 id
-- 去元表 Person 中找,Person 也没有 id
-- 再往上去 Person 的元表 Object 中找,找到 Object.id

person1.id = 100
print(person1.id) -- 100,person1 表自己有了 id 属性

person1:PrintId() -- 100
-- person1 自己没有 PrintId 方法
-- 会一直往上找到 Object 的 PrintId 方法
-- 使用 : 调用时,传入的是 person1 自己,所以打印 person1.id

Object:subClass("Monster") -- 另一个子类 Monster,和 Person 类似

local monster1 = Monster:new()

print(monster1.id) -- 1

monster1.id = 200
print(monster1.id) -- 200

monster1:PrintId() -- 200

这套写法本质是用元表链模拟继承。
对象找不到字段,就去类表找;类表找不到,就继续去父类表找。

多态

相同行为,不同表现,就是多态。
同名方法在不同类里不同实现,就是多态。

创建继承万物之父的GameObject类

Object:subClass("GameObject")

-- 设置 GameObject 的坐标属性
GameObject.posX = 0
GameObject.posY = 0

-- 设置 GameObject 移动方法
function GameObject:Move()
    self.posX = self.posX + 1
    self.posY = self.posY + 1

    print(self.posX)
    print(self.posY)
end

创建继承GameObject类的Player类

GameObject:subClass("Player")

-- 设置 Player 的移动方法
function Player:Move()
    -- Player 继承 GameObject,所以 Player 存在 base 属性
    -- base 指向 GameObject 表,也就是父类表

    -- Player 正常可能会想先调用父类 GameObject 的 Move 方法

    -- 如果使用冒号调用父类方法:
    -- self.base:Move()
    -- 实际上传进去的 self 会变成 GameObject
    -- 这样修改的就是 GameObject 表上的 posX / posY
    -- 有点像是在改一份公共的静态数据,而不是当前 player 对象自己的数据
    -- 所以这里不这么写

    -- 如果要执行父类逻辑,不直接使用冒号调用
    -- 用 . 调用父类方法,然后手动把当前对象 self 传进去
    -- 这样 GameObject:Move 里的 self 才会是当前实例化出来的 player
    self.base.Move(self)
end

local player1 = Player:new()
player1:Move() -- 1 1

local player2 = Player:new()
player2:Move() -- 1 1,不会影响到 player1

player1:Move() -- 2 2

这里是 Lua 面向对象里很容易混的点:
: 会自动把调用者当成第一个参数传进去,. 不会自动传。
所以调用父类方法时,想让父类方法操作当前对象,就用 self.base.Move(self)


15.2 知识点代码

Lesson15_面向对象.lua

print("**********面向对象************")

print("**********知识点一 封装************")
-- Lua 中没有 C# 那种原生 class 语法
-- Lua 里的面向对象,是用 table + function + metatable 模拟出来的
-- 表保存字段和方法,元表负责查找规则,冒号调用负责把当前对象当成 self 传进去

-- 创建万物之父类
Object = {}

-- Lua 中的类本质上还是表,没有 C# 那种原生实例化语法
-- 所以这里给 Object 手动添加一个 new 方法
function Object:new()
    -- 实例化方法的目的,是返回一张新的对象表
    -- Lua 中的对象就是为一张表

    local obj = {}          -- 要实例化的对象表,写成 local,避免变成全局变量

    setmetatable(obj, self) -- 设置当前类表为对象表的元表

    self.__index = self     -- 设置 __index 指向当前类表
                            -- 这样对象找不到字段或方法时,就会去类表中找

    return obj              -- 返回对象
end

-- 给万物之父设置一个属性 id
Object.id = 1

-- 给万物之父设置打印 id 属性的方法
function Object:PrintId()
    print(self.id)
end

local myObj = Object:new() -- 实例化对象

print(myObj)               -- table: 00BAA670,对象本质还是表

print(myObj.id)            -- 1
-- myObj 自己没有 id 属性
-- 所以会去 myObj 的元表 Object 的 __index 指向的 Object 中找 id

myObj:PrintId()            -- 1
-- myObj 自己没有 PrintId 方法
-- 也是去 Object 中找到 PrintId 方法
-- 使用 : 调用时,会把 myObj 作为第一个参数传进去,也就是方法里的 self

myObj.id = 2               -- 给当前对象设置一个自己的 id 属性

print(Object.id)           -- 1,Object 上的 id 没有被改动

print(myObj.id)            -- 2
-- myObj 自己已经有 id 了,就不会再去 Object 中找默认值

myObj:PrintId()            -- 2
-- PrintId 方法仍然来自 Object
-- 但 self 是 myObj,所以打印的是 myObj 自己的 id

print("***********知识点二 继承************")

-- 回顾 _G 表
-- 继承这里用到了 _G 表
-- _G 本质上也是一张表,以键值对的形式存储全局变量
print(_G) -- table: 00D54F00,_G 是一个表

-- 设置全局变量 a 的初始值为 111
_G["a"] = 111
print(a) -- 111

-- 修改全局变量 a 的值为 222
_G.a = 222
print(a) -- 222

-- 尝试将全局变量 a 的值作为 key,设置 _G 表中的值
-- 注意:这不会改变全局变量 a 本身
_G[a] = 333
print(a)       -- 222
print(_G[222]) -- 333

-- 修改全局变量 a 的值为字符串 "a"
_G.a = "a"
print(a)     -- a
print(_G[a]) -- a,这样写等同于 _G["a"],因为 a = "a"

-- 给万物之父设置继承的方法,要传入子类名,要双引号包裹
function Object:subClass(className)
    -- 继承的目的,是设置子类表和父类表之间的查找关系
    -- 这里通过元表建立继承关系

    -- 在 _G 中用传入的类名创建一张全局表
    _G[className] = {}

    -- 使用元表建立继承规则
    _G[className].base = self         -- base 表示父类表,self 表示当前调用 subClass 的类
    setmetatable(_G[className], self) -- 设置子类的元表为当前父类
    self.__index = self               -- 父类的 __index 指向父类自己
                                       -- 这样子类找不到字段和方法时,可以去父类中找
end

Object:subClass("Person")    -- 子类 Person 继承 Object

print(Person)                -- table: 00E5A4B8

local person1 = Person:new() -- 实例化 Person
-- Person 现在是空表,没有 new 方法
-- 去 Person 的元表 Object 的 __index 指向的 Object 中找,找到 new 方法
-- new 方法会把 Person 作为第一个参数传进去
-- Person 的 __index 会设置成 Person 自己
-- 返回一个实例化 Person 的对象,并且这个对象的元表是 Person

print(person1.id) -- 1
-- person1 自己没有 id
-- 去元表 Person 中找,Person 也没有 id
-- 再往上去 Person 的元表 Object 中找,找到 Object.id

person1.id = 100
print(person1.id) -- 100,person1 表自己有了 id 属性

person1:PrintId() -- 100
-- person1 自己没有 PrintId 方法
-- 会一直往上找到 Object 的 PrintId 方法
-- 使用 : 调用时,传入的是 person1 自己,所以打印 person1.id

Object:subClass("Monster") -- 另一个子类 Monster,和 Person 类似

local monster1 = Monster:new()

print(monster1.id) -- 1

monster1.id = 200
print(monster1.id) -- 200

monster1:PrintId() -- 200

print("**********知识点三 多态************")
-- 相同行为,不同表现,就是多态
-- 同一个方法名,不同类里有不同实现,就是多态

-- 创建继承万物之父的 GameObject 类
Object:subClass("GameObject")

-- 设置 GameObject 的坐标属性
GameObject.posX = 0
GameObject.posY = 0

-- 设置 GameObject 移动方法
function GameObject:Move()
    self.posX = self.posX + 1
    self.posY = self.posY + 1

    print(self.posX)
    print(self.posY)
end

-- 创建继承 GameObject 类的 Player 类
GameObject:subClass("Player")

-- 设置 Player 的移动方法
function Player:Move()
    -- Player 继承 GameObject,所以 Player 存在 base 属性
    -- base 指向 GameObject 表,也就是父类表

    -- Player 正常可能会想先调用父类 GameObject 的 Move 方法

    -- 如果使用冒号调用父类方法:
    -- self.base:Move()
    -- 实际上传进去的 self 会变成 GameObject
    -- 这样修改的就是 GameObject 表上的 posX / posY
    -- 有点像是在改一份公共的静态数据,而不是当前 player 对象自己的数据
    -- 所以这里不这么写

    -- 如果要执行父类逻辑,不直接使用冒号调用
    -- 用 . 调用父类方法,然后手动把当前对象 self 传进去
    -- 这样 GameObject:Move 里的 self 才会是当前实例化出来的 player
    self.base.Move(self)
end

local player1 = Player:new()
player1:Move() -- 1 1

local player2 = Player:new()
player2:Move() -- 1 1,不会影响到 player1

player1:Move() -- 2 2

15.3 练习题

用Lua实现面向对象,定义动物类,动物类有名字和说话方法,定义具体实现的狗类和猫类

定义万物之父基类

Object = {}

setmetatable(Object, Object)
Object.__index = Object

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

function Object:subClass(className)
    _G[className] = {}
    _G[className].base = self
    setmetatable(_G[className], self)
    self.__index = self
end

function Object:toString()
    return tostring(self)
end

object1 = Object:new()
print(object1:toString())

定义动物类,重写new方法

Object:subClass("Animal")

function Animal:new(animalName)
    local obj = Animal.base.new(self)
    obj.animalName = animalName
    return obj
end

function Animal:Speak()
    print("动物" .. self.animalName .. "开始叫")
end

定义狗类,重写说话方法

Animal:subClass("Dog")

function Dog:Speak()
    self.base.Speak(self)
    print("狗" .. self.animalName .. "开始旺旺叫")
end

dog1 = Dog:new("Spike")
dog1:Speak()

定义猫类,重写说话方法

Animal:subClass("Cat")

function Cat:Speak()
    self.base.Speak(self)
    print("猫" .. self.animalName .. "开始喵喵叫")
end

cat1 = Cat:new("Tom")
cat1:Speak()

运行打印结果

table: 00C297C8
动物Spike开始叫
狗Spike开始旺旺叫
动物Tom开始叫
猫Tom开始喵喵叫

15.4 练习题代码

print("**********面向对象练习题************")

Object = {}

setmetatable(Object, Object)
Object.__index = Object

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

function Object:subClass(className)
    _G[className] = {}
    _G[className].base = self
    setmetatable(_G[className], self)
    self.__index = self
end

function Object:toString()
    return tostring(self)
end

object1 = Object:new()
print(object1:toString())

Object:subClass("Animal")

function Animal:new(animalName)
    local obj = Animal.base.new(self)
    obj.animalName = animalName
    return obj
end

function Animal:Speak()
    print("动物" .. self.animalName .. "开始叫")
end

Animal:subClass("Dog")

function Dog:Speak()
    self.base.Speak(self)
    print("狗" .. self.animalName .. "开始旺旺叫")
end

dog1 = Dog:new("Spike")
dog1:Speak()

Animal:subClass("Cat")

function Cat:Speak()
    self.base.Speak(self)
    print("猫" .. self.animalName .. "开始喵喵叫")
end

cat1 = Cat:new("Tom")
cat1:Speak()


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏