Lua 面向对象
面向对象编程(Object Oriented Programming,OOP)是一种非常流行的计算机编程架构,通过创建和操作对象来设计应用程序。
以下几种编程语言都支持面向对象编程:
- C++
- Java
- Objective-C
- Smalltalk
- C#
- Ruby
Lua 是一种轻量级的脚本语言,虽然它不像 Java 或 C++ 那样内置强大的面向对象(OO)特性,但它非常灵活,可以通过一些技巧实现面向对象编程。
面向对象特征
封装:将数据和方法捆绑在一起,隐藏实现细节,只暴露必要的接口,提高安全性和可维护性。
继承:通过派生新类复用和扩展现有代码,减少重复编码,提高开发效率和可扩展性。
多态:同一操作作用于不同对象时表现不同,支持统一接口调用,增强灵活性和扩展性。
抽象:简化复杂问题,定义核心类和接口,隐藏不必要的细节,便于管理复杂性。
Lua 中面向对象
我们知道,对象由属性和方法组成。
Lua 中的类可以通过 table + function 模拟出来。
至于继承,可以通过 metetable 模拟出来(不推荐用,只模拟最基本的对象大部分实现够用了)。
在 Lua 中,最基本的结构是 table,我们可以使用表(table)来创建对象。
ClassName = {} -- 创建一个表作为类
通过 new 方法(或其他名称)创建对象,并初始化对象的属性。
function ClassName:new(...) local obj = {} -- 创建一个新的空表作为对象 setmetatable(obj, self) -- 设置元表,使对象继承类的方法 self.__index = self -- 设置索引元方法 -- 初始化对象的属性 obj:init(...) -- 可选:调用初始化函数 return obj end
表(table)是 Lua 中最基本的复合数据类型,可以用来表示对象的属性。
Lua 中的 function 可以用来表示方法:
function ClassName:sayHello() print("Hello, my name is " .. self.name) end
使用 new 方法来创建对象,并通过对象调用类的方法。
local obj = ClassName:new("Alice") -- 创建对象 obj:sayHello() -- 调用对象的方法
在 Lua 中,表(table)可以视为对象的一种变体。和对象一样,表具有状态(成员变量),并且可以代表独立的实体。
表不仅具有数据成员,还可以包含与对象方法类似的成员函数:
实例
Person = {name = "", age = 0}
-- Person 的构造函数
function Person:new(name, age)
local obj = {} -- 创建一个新的表作为对象
setmetatable(obj, self) -- 设置元表,使其成为 Person 的实例
self.__index = self -- 设置索引元方法,指向 Person
obj.name = name
obj.age = age
return obj
end
-- 添加方法:打印个人信息
function Person:introduce()
print("My name is " .. self.name .. " and I am " .. self.age .. " years old.")
end
代码说明:
- Person 是一个表,它有两个属性:name 和 age,这两个属性是类的默认属性。
- Person:new(name, age) 是一个构造函数,用来创建新的 Person 对象。
- local obj = {} 创建一个新的表作为对象,setmetatable(obj, self) 设置元表,使得该表成为 Person 类的实例。
- self.__index = self 设置索引元方法,使得 obj 可以访问 Person 类的属性和方法。
- introduce 是 Person 类的方法,打印该 Person 对象的名字和年龄。
调用方法:
-- 创建一个 Person 对象 local person1 = Person:new("Alice", 30) -- 调用对象的方法 person1:introduce() -- 输出 "My name is Alice and I am 30 years old."
一个简单实例
以下简单的类包含了三个属性: area, length 和 breadth,printArea方法用于打印计算结果:
实例
Rectangle = {area = 0, length = 0, breadth = 0}
-- 创建矩形对象的构造函数
function Rectangle:new(o, length, breadth)
o = o or {} -- 如果未传入对象,创建一个新的空表
setmetatable(o, self) -- 设置元表,使其继承 Rectangle 的方法
self.__index = self -- 确保在访问时能找到方法和属性
o.length = length or 0 -- 设置长度,默认为 0
o.breadth = breadth or 0 -- 设置宽度,默认为 0
o.area = o.length * o.breadth -- 计算面积
return o
end
-- 打印矩形的面积
function Rectangle:printArea()
print("矩形面积为 ", self.area)
end
创建对象
创建对象是为类的实例分配内存的过程,每个类都有属于自己的内存并共享公共数据:
r = Rectangle:new(nil,10,20)
访问属性
我们可以使用点号 .来访问类的属性:
print(r.length)
访问成员函数
我们可以使用冒号 : 来访问类的成员函数:
r:printArea()
内存在对象初始化时分配。
完整实例
以下我们演示了 Lua 面向对象的完整实例:
实例
Rectangle = {area = 0, length = 0, breadth = 0}
-- 创建矩形对象的构造函数
function Rectangle:new(o, length, breadth)
o = o or {} -- 如果未传入对象,创建一个新的空表
setmetatable(o, self) -- 设置元表,使其继承 Rectangle 的方法
self.__index = self -- 确保在访问时能找到方法和属性
o.length = length or 0 -- 设置长度,默认为 0
o.breadth = breadth or 0 -- 设置宽度,默认为 0
o.area = o.length * o.breadth -- 计算面积
return o
end
-- 打印矩形的面积
function Rectangle:printArea()
print("矩形面积为 ", self.area)
end
-- 运行实例:
local rect1 = Rectangle:new(nil, 5, 10) -- 创建一个长为 5,宽为 10 的矩形
rect1:printArea() -- 输出 "矩形面积为 50"
local rect2 = Rectangle:new(nil, 7, 3) -- 创建一个长为 7,宽为 3 的矩形
rect2:printArea() -- 输出 "矩形面积为 21"
执行以上程序,输出结果为:
矩形面积为 50 矩形面积为 21
Lua 继承
继承是指一个对象直接使用另一对象的属性和方法,可用于扩展基础类的属性和方法。
Lua 中的继承通过设置子类的元表来实现。
我们可以创建一个新表,并将其元表设置为父类。
以下实例 Square 类将继承 Rectangle 类的属性和方法,并在其基础上做出改动。
Rectangle = {area = 0, length = 0, breadth = 0}
-- 创建矩形对象的构造函数
function Rectangle:new(o, length, breadth)
o = o or {} -- 如果未传入对象,创建一个新的空表
setmetatable(o, self) -- 设置元表,使其继承 Rectangle 的方法
self.__index = self -- 确保在访问时能找到方法和属性
o.length = length or 0 -- 设置长度,默认为 0
o.breadth = breadth or 0 -- 设置宽度,默认为 0
o.area = o.length * o.breadth -- 计算面积
return o
end
-- 打印矩形的面积
function Rectangle:printArea()
print("矩形面积为 ", self.area)
end
-- 定义正方形类,继承自矩形类
Square = Rectangle:new() -- Square 继承 Rectangle 类
-- 重写构造函数(正方形的边长相等)
function Square:new(o, side)
o = o or {} -- 如果未传入对象,创建一个新的空表
setmetatable(o, self) -- 设置元表,使其继承 Rectangle 的方法
self.__index = self -- 确保在访问时能找到方法和属性
o.length = side or 0 -- 设置边长
o.breadth = side or 0 -- 正方形的宽度和长度相等
o.area = o.length * o.breadth -- 计算面积
return o
end
-- 运行实例:
local rect = Rectangle:new(nil, 5, 10) -- 创建一个长为 5,宽为 10 的矩形
rect:printArea() -- 输出 "矩形面积为 50"
local square = Square:new(nil, 4) -- 创建一个边长为 4 的正方形
square:printArea() -- 输出 "矩形面积为 16"
Rectangle
类:依然是矩形的基本类,拥有 length
、breadth
和 area
属性,以及计算和打印面积的方法。
Square
类继承自 Rectangle
:Square
类通过 Rectangle:new()
来继承 Rectangle
类的方法和属性。由于正方形的长度和宽度相等,我们在 Square:new
方法中重写了构造函数,将 length
和 breadth
设置为相同的值(即 side
)。
重写构造函数:Square:new(o, side)
方法创建正方形对象时,使用传入的边长 side
初始化 length
和 breadth
属性,并计算面积。
运行结果:
矩形面积为 50 矩形面积为 16
函数重写
在 Lua 中,函数重写(也称为方法重写)指的是在继承过程中,子类对父类中已有方法的重新定义或替换。
子类可以根据需要修改或扩展父类的方法行为。
以上实例中 Square 类重写了 Rectangle 类的构造函数,从而改变了对象的初始化方式,特别是将矩形的 length 和 breadth 设为相同的值,因为正方形的特性是边长相等。
接下来我们通过一个 Animal 类和一个继承自它的 Dog 类,展示如何重写方法。
Animal = {name = "Unknown"}
-- Animal 类的构造函数
function Animal:new(o, name)
o = o or {} -- 如果没有传入对象,则创建一个新的空表
setmetatable(o, self) -- 设置元表,使其继承 Animal 的方法
self.__index = self -- 让对象可以访问 Animal 的方法
o.name = name or "Unknown" -- 设置名称,默认为 "Unknown"
return o
end
-- Animal 类的方法:叫声
function Animal:speak()
print(self.name .. " makes a sound.")
end
-- 定义狗类(Dog),继承自 Animal
Dog = Animal:new() -- Dog 继承 Animal 类
-- 重写狗类的构造函数
function Dog:new(o, name, breed)
o = o or {} -- 如果没有传入对象,则创建一个新的空表
setmetatable(o, self) -- 设置元表,使其继承 Dog 和 Animal 的方法
self.__index = self -- 让对象可以访问 Dog 的方法
o.name = name or "Unknown"
o.breed = breed or "Unknown"
return o
end
-- 重写狗类的叫声方法(重写 Animal 的 speak 方法)
function Dog:speak()
print(self.name .. " barks.")
end
-- 创建 Animal 对象
local animal = Animal:new(nil, "Generic Animal")
animal:speak() -- 输出 "Generic Animal makes a sound."
-- 创建 Dog 对象
local dog = Dog:new(nil, "Buddy", "Golden Retriever")
dog:speak() -- 输出 "Buddy barks."
Animal
类:定义了一个基础类Animal
,具有name
属性和speak
方法。speak
方法是一个默认的实现,输出"某个动物发出声音"。Dog
类继承Animal
:Dog
类继承自Animal
,并通过Dog:new()
方法创建自己的实例。重写
speak
方法:在Dog
类中,重写了speak
方法,将其行为从父类的"发出声音"改为"狗狗叫"。这就是方法重写的体现,子类(Dog
)改变了父类(Animal
)方法的行为。
运行结果:
Generic Animal makes a sound. Buddy barks.
多态
Lua 的多态性通过元表和方法重写实现。当不同类型的对象调用相同的方法时,Lua 会根据对象的实际类型执行不同的方法。
实例
Person = {}
-- 为"类"添加一个构造函数
function Person:new(name, age)
local obj = {} -- 创建一个新的表作为对象
setmetatable(obj, self) -- 设置元表,表示它是Person类的实例
self.__index = self -- 设置索引元方法,指向Person
obj.name = name
obj.age = age
return obj
end
-- 添加方法
function Person:greet()
print("Hello, my name is " .. self.name)
end
-- 定义一个子类 Student 继承自 Person
Student = Person:new()
-- 子类重写父类的方法
function Student:greet()
print("Hi, I'm a student and my name is " .. self.name)
end
local person2 = Person:new("Charlie", 25)
local student2 = Student:new("David", 18)
-- 多态:不同类型的对象调用相同的方法
person2:greet() -- 输出 "Hello, my name is Charlie"
student2:greet() -- 输出 "Hi, I'm a student and my name is David"
尽管 person2 和 student2 调用了同一个 greet 方法,但由于它们的类型不同,Lua 会调用各自适合的版本。
运行结果:
Hello, my name is Charlie Hi, I'm a student and my name is David
其他面向对象的概念
封装
封装通常通过将数据和方法封装在一个表中实现。我们可以通过控制表的访问权限来模拟封装,例如使用 metamethods 来限制外部访问。
实例
Person = {}
-- 添加封装:隐藏属性
function Person:new(name, age)
local obj = {}
setmetatable(obj, self)
self.__index = self
obj.name = name
obj.age = age
return obj
end
function Person:setName(name)
self.name = name -- 提供方法来修改 name
end
function Person:getName()
return self.name -- 提供方法来获取 name
end
通过这种方式,我们可以控制属性的访问,模拟封装。
抽象
抽象指的是简化复杂的事物,将不需要的细节隐藏。虽然 Lua 本身没有类的概念,但我们可以通过封装来达到抽象的目的。
实例
function Person:showInfo()
print("Name: " .. self.name)
print("Age: " .. self.age)
end