一、面向对象
面向对象编程(Object Oriented Programming,OOP)是一种非常流行的计算机编程架构。
1. 特征
封装: 指能够把一个实体的信息、功能、响应都装入一个单独的对象中的特性。
继承: 继承的方法允许在不改动原程序的基础上对其进行扩充,这样使得原功能得以保存,而新功能也得以扩展。这有利于减少重复编码,提高软件的开发效率。
多态: 同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。在运行时,可以通过指向基类的指针,来调用实现派生类中的方法。
抽象: 抽象(Abstraction)是简化复杂的现实问题的途径,它可以为具体问题找到最恰当的类定义,并且可以在最恰当的继承级别解释问题。
二、Lua 中面向对象
Lua语言本身并没有提供面向对象的语法机制,这需要我们自己设计实现一套类的机制。首先,对于面向对象来说,我们至少需要类和对象这两个概念。
我们知道,对象由属性和方法组成。Lua中最基本的结构是table,所以需要用table来描述对象的属性。
Lua中的 function 可以用来表示方法。那么Lua中的类可以通过 table + function 模拟出来。
类至少包含一个用于构造对象的方法。 对应到Lua上,就是一个代表类的table,它有一个构造函数,返回代表该类对象的table:
Class = {} function Class:new() local o = {} return o end local object = Class:new()
1. 类的封装
-- 元类
rect = { area = 0, length = 0, breadth = 0 } -- 派生类的方法 new function rect:new(o, length, breadth) o = o or {} setmetatable(o, self) self.__index = self self.length = length or 0 self.breadth = breadth or 0 self.area = length * breadth return o end -- 派生类的方法 printArea function rect:printArea() print('rect\'s area = ', self.area) end
(1) 创建对象
创建对象是为类的实例分配内存的过程。每个类都有属于自己的内存并共享公共数据。
r = rect:new(nil, 10, 20)
(2) 访问属性
我们可以使用点号(.)来访问类的属性:
r:printArea()
(3) 访问成员函数
我们可以使用冒号 : 来访问类的成员函数:
r:printArea()
内存在对象初始化时分配。
2. 类的继承
面向对象的一大特点就是继承,Lua的元表跟元方法也能模拟。
【Person】:
--[[ filename: '1_test.lua' ]]-- Person = { age = 18, name = 'cauchy', } function Person:new(age, name) local o = {} setmetatable(o, self) self.__index = self o.age = age o.name = name return o end function Person:getAge() return self.age end function Person:printf() print(self.age, self.name) end
Teacher继承了Person,并重写了Print()方法。
【Teacher】:
--[[ filename: '2_test.lua' ]]-- require "1_test" Teacher = Person:new() function Teacher:new(age, name, course) local o = Person:new(age, name) setmetatable(o, self) self.__index = self o.course = course return o end function Teacher:printf() print(self.age, self.name, self.course) end local t = Teacher:new(30, 'Miss Liu', 'English') t:printf() print(t:getAge())
运行结果:
30 Miss Liu English 30
Teacher并没有重写GetAge()方法,然而 t1:GetAge() 却正确的输出了30,所以是正确的继承了这个方法。
而重写的printf()方法,多输出了一个course,也正常输出,可见重写也是可以的。
3. 类的多态
多态:同一个实现接口,使用不同的实例而执行不同的操作。
Object = {} -- 所有对象基类: Object function Object:new() -- 实例化方法 local obj = {} setmetatable(obj, self) self.__index = self return obj end function Object:create(cls) -- 继承 _G[cls] = {} -- 大G表通过键值对,存储所有的全局变量 local class = _G[cls] setmetatable(class, self) self.__index = self class.base = self -- 设置base属性,方便子类找父类 end --[[ 1. 生成GameObject类,为Object子类。 (1) 两个属性:(posX, posY) (2) 一个方法:(move()) --]] Object:create("GameObject") -- create object (object -> GameObject) GameObject.posX, GameObject.posY = 0, 0 -- set attribute function GameObject:move() -- set method print(string.format('move: (%s)', self)) self.posX = self.posX + 1 self.posY = self.posY + 1 print("posX: ", self.posX, " posY: ", self.posY) end --[[ 多态 1. 生成Player类,为GameObject子类。 (1) 重写move()方法 --]] GameObject:create("Player") -- create object (GameObject -> Player) function Player:move() -- set method --[[ 这样调用即GameObject:move() 实例化出来的Player对象都共用GameObject的posX, posY。 --]] -- self.base:move() --[[ 这样实际传入的self是Player 但是Player里没有posX, posY属性,会去找GameObject,赋值自己的posX, posY,不会相互影响。 --]] self.base.move(self) end local p1 = Player:new() local p2 = Player:new() p1:move() p2:move() p1:move()
运行结果:
move: (table: 0x55fdb0457b70) posX: 1 posY: 1 move: (table: 0x55fdb0457bb0) posX: 1 posY: 1 move: (table: 0x55fdb0457b70) posX: 2 posY: 2
三、方法访问权限(私有公有)
冒号的作用是省略了self的传递,也就是说,如果我们不想写冒号改为写等号的话,那么每个方法的第一个参数必然是self。
冒号的作用有两个:
1、方法定义:会增加一个额外的隐藏形参(self)
2、方法调用:会增加一个额外的实参(表自身)
local t = { a = 1, b = 2 } function t:add() -- 使用 : 自定义函数 return self.a + self.b end function t.sub(self) -- 使用 . 自定义函数 return self.a - self.b end print(t:add()) print(t.add(t)) print(t.sub(t)) print(t:sub())
以上两种方法都可以说是Public方法。
local function Console(self) -- 私有方法 Console print(self.a, self.b) end
私有方法同样需要传递这个self,区别在于,私有方法是用local写的。跟其他语言不同之处在于,Lua中模拟的私有方法并没有确定的归属,换句话说,它只属于其所写的Lua文件,而不是写在文件中的某个Table表。