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