Skip to content

Lua 面向对象

面向对象编程(Object-Oriented Programming, OOP)是一种基于“对象”概念的编程范式。对象可以包含以字段形式(通常称为属性或特性)存在的数据,以及以过程形式(通常称为方法)存在的代码。OOP 的关键特性包括:

  • 对象(Objects): 捆绑状态(数据)和行为(方法)的实例。
  • 类(Classes): 创建对象的蓝图或模板(在基于原型(prototype-based)的 Lua 中不那么直接)。
  • 封装(Encapsulation): 将数据和操作数据的方法捆绑在一个单元内,并限制对对象某些组件的直接访问。
  • 继承(Inheritance): 新类(或 Lua 中的对象)从现有类继承属性和方法的机制。
  • 多态(Polymorphism): 对象可以呈现多种形式的能力,通常允许方法根据作用于的对象执行不同的操作。

Lua 没有像 Java 或 C++ 那样的内置类。相反,Lua 中的 OOP 通常使用表和元表来实现,遵循基于原型的方法。对象直接从其他对象(它们的原型)继承。

  • 对象即表: Lua 对象通常由一个表表示。其字段存储对象的状态,其方法是存储在表(或其原型)中的函数。
  • 原型和 __index: 通过设置表的元表并使用 __index 元方法(metamethod)指向其原型来实现继承。如果在对象中找不到字段或方法,Lua 会通过 __index 在原型中查找。
  • self 约定: 方法通常使用冒号运算符(:)定义和调用,这会隐式地(implicitly)将对象自身作为第一个参数传递,按照约定命名为 self。

一个简单的类似类的结构:Rectangle

Section titled “一个简单的类似类的结构:Rectangle”

让我们为 Rectangle 创建一个“类”(原型表)。

-- Rectangle.lua(我们的原型或“类”表)
local Rectangle = {}
Rectangle.__index = Rectangle -- 继承的关键:方法在 Rectangle 本身中查找
-- 构造函数(按照约定命名为 'new')
function Rectangle:new(initial_length, initial_breadth)
local o = {} -- 创建一个空的表(即实例)
setmetatable(o, self) -- 将其元表设置为 Rectangle(或派生类)
-- 在这里,当作为 Rectangle:new() 调用时,self 将是 Rectangle
o.length = initial_length or 0
o.breadth = initial_breadth or 0
return o
end
-- 计算面积的方法
function Rectangle:calculateArea()
return self.length * self.breadth
end
-- 打印面积的方法
function Rectangle:printArea()
print("The area of the rectangle is: ", self:calculateArea())
end
-- 要使其成为一个模块,通常会返回 Rectangle 表
-- return Rectangle

要使用我们的 Rectangle“类”,首先需要确保它已加载(例如,如果它在单独的文件中,则通过 require 加载,或者仅在同一作用域(scope)内定义)。

-- 假设上面定义的 Rectangle 可用
-- 创建 Rectangle 的一个实例(对象)
local rect1 = Rectangle:new(10, 5)
-- 访问属性
print("Rectangle 1 Length:", rect1.length) -- Output: 10 -- 输出:10
print("Rectangle 1 Breadth:", rect1.breadth) -- Output: 5 -- 输出:5
-- 调用方法
rect1:printArea() -- Output: The area of the rectangle is: 50 -- 输出:矩形的面积是: 50
local rect2 = Rectangle:new(7, 3)
rect2:printArea() -- Output: The area of the rectangle is: 21 -- 输出:矩形的面积是: 21

self 和 : 的工作原理:

  • 当你定义 function Rectangle:new(...) 时,它是 Rectangle.new = function(self, ...) 的语法糖(syntactic sugar)。
  • 当你调用 rect1:printArea() 时,它是 rect1.printArea(rect1) 的语法糖。

让我们首先定义一个更通用(generic)的 Shape 原型,然后让 Square 从它继承。

-- Shape.lua(基础原型)
local Shape = {}
Shape.__index = Shape
function Shape:new()
local o = {}
setmetatable(o, self)
return o
end
function Shape:getType()
return "Generic Shape" -- 通用形状
end
-- Square.lua(派生原型)
local Square = Shape:new() -- 创建 Square 对象,最初从 Shape 继承
-- (或者 Square = setmetatable({}, Shape) 然后 Square.__index = Square 用于 Square 特有的方法)
Square.__index = Square -- 实例中找不到的方法先在 Square 中查找,然后通过元表链在 Shape 中查找
setmetatable(Square, Shape) -- Square 的元表是 Shape,所以 Square “是”一个 Shape 原型
-- Square 自己的构造函数
function Square:new(side)
local o = Shape.new(self) -- 调用 Shape 的 'new',但要确保元表设置为 Square
-- 更健壮的方式:local o = {}; setmetatable(o, self)
o.side = side or 0
return o
end
-- 重写 Shape 的 getType 方法
function Square:getType()
return "Square" -- 正方形
end
-- Square 特有的方法
function Square:calculateArea()
return self.side * self.side
end
function Square:printArea()
print("The area of the square (side " .. self.side .. ") is: " .. self:calculateArea()) -- 正方形(边长为 .. self.side .. )的面积是: .. self:calculateArea()
end
-- 示例用法:
local generic_shape = Shape:new()
print(generic_shape:getType()) -- Output: Generic Shape -- 输出:通用形状
local my_square = Square:new(7)
print(my_square:getType()) -- Output: Square (overridden method) -- 输出:正方形(重写的方法)
my_square:printArea() -- Output: The area of the square (side 7) is: 49 -- 输出:正方形(边长为 7)的面积是:49
-- 演示 Square 本身没有的方法的继承链:
-- 如果 Square 有一个像 { __index = Shape } 这样的元表,并且 Square.getType 是 nil,
-- my_square:getType() 将解析为 Shape:getType()。
-- 我们当前的设置 Square.__index = Square; setmetatable(Square, Shape) 为“类”方法实现了这一点
-- 并且实例方法通过 instance_mt.__index = Square 解析。

解释 Square 的继承:

  1. local Square = Shape:new() 或更明确地说 Square = {}。
  2. setmetatable(Square, { __index = Shape })。这使得 Square 继承自 Shape。如果 Square 中没有一个键,Lua 会在 Shape 中查找。
  3. Square.__index = Square。对于 Square 的实例,如果在实例中没有一个键,Lua 会在 Square 表本身中查找。
  4. 构造函数 Square:new(side):创建一个实例 o,将其元表设置为 Square(因此 o 继承自 Square),并初始化其特定属性。
一个更常见的与 `Shape:new` 相关的 `Square:new` 模式:
-- 更健壮的 Square 构造函数,假设 Shape 有一个构造函数 Shape:new(o_template)
-- Shape.lua -- Shape.lua
-- ...(之前的 Shape 原型)
-- function Shape:new(o_template)
-- o_template = o_template or {}
-- setmetatable(o_template, self)
-- self.__index = self -- 确保实例在其类原型中查找方法
-- return o_template
-- end
-- Square.lua -- Square.lua
-- ...(Square 原型设置,Square.__index = Square,setmetatable(Square, {__index = Shape}))
-- function Square:new(side)
-- local o = Shape:new(self) -- 这里的 'self' 是 Square,因此 Shape:new 正确地为 Shape 部分设置了元表
-- -- 然而,o 的元表现在是 Shape。我们需要它成为 Square。
-- -- 一个更好的方法:
-- local o = {}
-- setmetatable(o, self) -- self 是 Square,因此 o 的元表指向 Square
-- Shape.initialize(o) -- 如果 Shape 有一个 initialize 方法(可选)
-- o.side = side or 0
-- return o
-- end
-- Square 继承 Rectangle(来自第一个示例)的最简单模式:
-- 最初定义的 Rectangle 原型
-- Square 原型
local Square = {}
setmetatable(Square, {__index = Rectangle}) -- Square 继承自 Rectangle
Square.__index = Square -- Square 的实例首先在 Square 中查找方法
function Square:new(side)
local o = {}
setmetatable(o, self) -- 当作为 Square:new() 调用时,self 是 Square 表
o.length = side -- 如果复用 Rectangle 的方法,使用 Rectangle 期望的字段
o.breadth = side
-- o.side = side -- 或者使用它自己的字段
return o
end
-- 可选地重写方法
function Square:printArea()
-- 如果使用 .side:
-- print("The area of the square is: ", self.side * self.side) -- 打印("正方形的面积是:", self.side * self.side)
-- 如果通过继承的字段复用 Rectangle 的 calculateArea:
print("The area of the square is: ", Rectangle.calculateArea(self)) -- 打印("正方形的面积是:", Rectangle.calculateArea(self))
end
local sq = Square:new(4)
sq:printArea() -- 将调用 Square 的 printArea
-- 如果 Rectangle 的 calculateArea 没有被覆盖,则调用它:
-- print(Rectangle.calculateArea(sq)) -- 打印(Rectangle.calculateArea(sq))

实际应用: Lua 中的 OOP 在游戏开发中被广泛用于角色实体、UI 元素(UI elements)和游戏系统。在大型应用程序中,它也用于将代码组织成易于管理和可重用组件(reusable components)。

深入学习: 探索 mixins、多重继承(可以通过 __index 函数模拟(emulated))以及 Lua 中不同的 OOP 实现模式(implementation patterns)等概念。Roberto Ierusalimschy 撰写的书籍《Programming in Lua》提供了关于此主题的优秀章节。