面向对象的三个特征:封装、继承和多态。Lua并没有类,没有直接实现面向对象的方法。不过Lua的Table,有内部对象和内部方法。Lua的面向对象主要是通过Table来模拟面向对象。


封装

Lua之Table学习中,曾以Computer为例子,介绍了Table 也是可以有自己内部的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
local CPU = { name = 'CPU', cost = 400 }
local Monitor = { name = 'bird', cost = 200 }
local Memory = { name = 'memory', cost = 100 }
local Computer = {
description = "this is a computer",
cpu = CPU,
monitor = Monitor,
memory = Memory
}
function Computer.getDescription()
return Computer.description
end
print(Computer.getDescription())
>

上面的代码有几个问题
1、没有构造函数,没办法实例化对象
2、内部的方法更像Java中的静态方法,不需要有具体对象都能够调用。内部的成员更像静态成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
local Computer={}
Computer.cpu = { name = 'cpu',cost = 400 }
Computer.memory = {name = 'memory',cost = 200 }
Computer.monitor = {name = 'monitor',cost = 100 }
Computer.description = 'this is a computer'
function Computer:getDescription() --内部方法
print(self.description)
end
function Computer:new () --构造函数
t = {}
setmetatable(t, { __index = self })
return t
end
function Computer:getCost() --内部方法
cost = self.cpu.cost + self.memory.cost + self.monitor.cost
print(self.description..':cost '.. cost)
end

c1=Computer:new()
c1.description = 'this is c1' --改变Computer实例化对象内部的description
c1:getDescription()
c1:getCost()

c2=Computer:new()
c2.memory.cost = 900 --改变Computer实例化对象内部的memory.cost
c2:getDescription()
c2:getCost()

输出

1
2
3
4
this is c1  
this is c1:cost 700 --c2的memory.cost改变,c1不受影响
this is a computer --c1的description改变,c2不受影响
this is a computer:cost 1400

改良后的代码,有了构造函数,能够实例化对象。上面实例化了两个对象c1和c2,内部成员相互独立,无法相互影响。

访问成员

使用点访问类内成员

1
c1.description = 'this is c1'

成员函数

1
2
3
4
5
6
7
8
function getCost()  --普通函数
--do something
end

function Computer:getCost() --成员函数,
cost = self.cpu.cost + self.memory.cost + self.monitor.cost
print(self.description..':cost '.. cost)
end

普通函数只需要function修饰就可以了,成员函数除了function 关键字还需要声明属于某个类。前面的Computer 表示这个是Computer 类的成员函数。之间需要用冒号(:)来连接。
成员函数内部的self相当于Java 的this,表示实例对象自身。
如果用普通函数声明为成员函数,调用自身成员需要传入self

1
2
3
function Computer.getDescription(self) --成员函数
print(self.description)
end

实例对象访问成员函数通过冒号(:)访问,也可以通过点号(.)访问,后者访问时候需要传入self,即自身

1
2
3
4
5
6
7
8
9
10
11
12
13
function Computer.getDescription(self)
print(self.description)
end

function Computer:getCost()
cost = self.cpu.cost + self.memory.cost + self.monitor.cost
print(self.description .. ':cost ' .. cost)
end

c1:getCost()
c1.getCost(c1)
c1:getDescription()
c1.getDescription(c1)

输出

1
2
3
4
this is c1:cost 700 
this is c1:cost 700 --只用冒号(:)式声明getCost(),用点式(.)传入自身也能访问
this is c1 --只用点式(.)式声明getCost(),用冒号(:)式不传参也能访问
this is c1

可以看到两种成员函数声明方式是等价的,使用也是等价的。

构造函数

使用构造函数来为类的实例分配内存,每个类都有属于自己的内存并共享公共数据。

1
2
3
4
5
function Computer:new ()  --构造函数
t = {}
setmetatable(t, { __index = self })
return t
end

构造函数本质上也是成员函数,同时需要使用关键字new 来说明这是一个构造函数。

为什么要用元表来构造呢,不能直接return self呢,下面举个例子,构造函数直接返回self

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local Computer = {}
Computer.description = 'this is a computer'

function Computer:getDescription()
print(self.description)
end

function Computer:new()
return self
end

c1 = Computer:new()
c1.description = 'this is c1'
c1:getDescription()
c2 = Computer:new()
c2:getDescription()

输出

1
2
this is c1
this is c1

按照思维,只改变了c1的description,c1和c2是独立的对象,c2应该不受影响才对。编译过程也没有报错,但是c2的description也被改变了。
如果我们把self本身也看做一块独立的内存,实例化c1的时候,构造函数返回self,c1 -> self,对象c1指向self的内存位置。在实例化c2的时候,构造函数也返回了self,c2也指向了self。改变c1的description,实质改变了self的description,这样c2读取的description,也受到了改变。

如何正确地构造对象呢
1
2
3
4
5
function Computer:new()
t = {}
setmetatable(t, { __index = self })
return t
end

这个构造函数,先是构造了一个空表t,然后把self传入到__index元方法,作为元表。最后返回t对象。

Lua查找一个表元素时的规则,其实就是如下3个步骤:
1.在表中查找,如果找到,返回该元素,找不到则继续
2.判断该表是否有元表,如果没有元表,返回nil,有元表则继续。
3.判断元表有没有__index 方法,如果__index 方法为nil,则返回nil;如果__index方法是一个表,则重复1、2、3;如果__index方法是一个函数,则返回该函数的返回值。

Lua的“面向对象”,实质上是利用表和元表制造的一层层“嵌套关系”。c1.description = 'this is c1'这行函数,本质上没有改变self的值,而是在c1对象中增加了description这一属性,因为在表中已经找到了,所以就不会访问元表中的description属性,造成‘改写’这现象。而如果访问的是cpu这个属性,因为c1中找不到,会访问元表,然后直到找到为止。如果访问gpu这个不存在的属性,会一直找下去,直到元方法__index为nil时退出。
下面的代码应该可以验证这一猜想。

1
2
3
4
5
6
7
8
9
10
11
12
c1 = Computer:new()
print('before init description')
for k, v in pairs(c1) do
print(k, v)
end
print(c1.description)
c1.description = 'this is c1'
print('after init description')
for k, v in pairs(c1) do
print(k, v)
end
print(c1.description)

输出:在c1.description = 'this is c1'前key-value是没有任何输出的,说明c1没有这些东西(不包括元表的部分),但是又确确实实能输出(因为找不到就会访问元表)。

1
2
3
4
5
before init description
this is a computer
after init description
description this is c1
this is c1

上面是无参的构造函数,如果是有参的,需要在构造函数中同时改变self中的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Computer:new(description)
local o = {}
setmetatable(o, self)
self.__index = self
self.description = description
return o
end

c1 = Computer:new('init description')
print('c1 before', c1.description) --c1 before init description
c1.description = 'this is c1'
print('c1 after', c1.description) --c1 after this is c1
c2 = Computer:new()
print('c2', c2.description) --c2 nil


继承

在介绍Lua封装的时候,说过封装利用的是表与元表之间的‘层层嵌套’关系。这不多不少也体验了继承的思想。Lua的继承正是基于这种‘层层嵌套’的思想。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
local Shape = {} --定义基类
Shape.area = 0 --基类的成员area
function Shape:new()
local s = {}
setmetatable(s, self)
self.__index = self
return s
end

function Shape:getArea() --父类方法,可见子类也能访问
return self.area
end

local Square = Shape:new() --关键语句,Square继承Shape
function Square:new(side) --Square的构造函数
local s = {}
setmetatable(s, self)
self.__index = self --关键语句,元方法__index为空时候不会向元表查找area值
s.side = side or 0 --Square自身定义了成员变量side
self.area = side * side --改变基类的area的值
return s
end

local square = Square:new(10)
print('square side', square.side) --square side 10
print('square area', square:getArea()) --square area 100

这个例子,定义了一个基类图像Share,拥有成员area,然后正方形Square继承了Share,增加成员变量side,并重写构造函数。派生类拥有父类的方法和属性。
对比下Java的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
class Share {
double area = 0;
double getArea(){
return area;
}
}
class Square extends Share {
double size;
Square(double size) {
this.size = size;
area = size * size;
}
}

多态

上面例子中,面积的计算放在了构造函数中,总有一点不妥。假装我们对面向对象思想很熟悉了,写出的Java代码应该是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
abstract class Share {
double area = 0;
abstract void calcArea();
}
class Square extends Share {
double size;
Square(double size) {
this.size = size;
}
@Override
void calcArea() {
area = size * size;
}
}

class Circle extends Share {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override
void calcArea() {
area = 3.14 * radius * radius;
}
}

通过对calcArea的不同实现,在不同的子类完成不同的计算。这是多态的体现。Lua也可以,子类通过重写父类的方法实现不同的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
local Shape = {}
Shape.area = 0
function Shape:new()
local s = {}
setmetatable(s, self)
self.__index = self
return s
end

function Shape:getArea()
return self.area
end

local Square = Shape:new()
function Square:new(side)
local s = {}
setmetatable(s, self)
self.__index = self
s.side = side or 0
return s
end

function Square:getArea()
return 'I am Square s function', self.side * self.side
end

local square = Square:new(10)
print('square side', square.side)
print('square area', square:getArea())

上面的代码有两个getArea函数,其中子类Square复写了父类Share的getArea,并按新内容输出。

1
2
square side	10
square area I am Square s function 100

参考链接

lua中类的实现原理和实践
Lua 面向对象
Lua面向对象编程详解