前置知识: Lua

面向对象编程

2 minIntermediate2026/6/14

Lua OOP实现

概述

Lua 没有内置的和继承机制,但通过元表和 __index 元方法可以优雅地实现面向对象编程。本文介绍 Lua 中、继承、多态和封装的常见实现模式。

兯础概念

Lua OOP 的核心原理

  • 表即对象:Lua 的表是唯一的复合数据结构,可以充当对象
  • __index 实现继承:当表访问不存在的键时,__index 指向的表会被查找
  • self 引用:使用冒号语法(:)自动传入 self 参数
  • 原型链:通过 __index 链实现似原型继承的效果

快速上手

基本定义

-- 类定义模板
local Class = {}
Class.__index = Class

-- 构造函数
function Class.new(x, y)
    -- setmetatable 将 Class 作为元表,__index 指向 Class
    local self = setmetatable({}, Class)
    self.x = x or 0
    self.y = y or 0
    return self
end

-- 实例方法(使用冒号语法,自动传入 self)
function Class:area()
    return self.x * self.y
end

function Class:describe()
    return string.format("矩形 %dx%d, 面积=%d", self.x, self.y, self:area())
end

-- 使用
local obj = Class.new(3, 4)
print(obj:area())     -- 12
print(obj:describe()) -- 矩形 3x4, 面积=12

继承

-- 基类
local Animal = {}
Animal.__index = Animal

function Animal.new(name)
    local self = setmetatable({}, Animal)
    self.name = name
    return self
end

function Animal:speak()
    return self.name .. " 发出声音"
end

-- 子类
local Dog = setmetatable({}, { __index = Animal })
Dog.__index = Dog

function Dog.new(name, breed)
    local self = Animal.new(name) -- 调用父类构造函数
    setmetatable(self, Dog)       -- 设置子类元表
    self.breed = breed
    return self
end

-- 重写方法
function Dog:speak()
    return self.name .. " 汪汪!"
end

-- 新增方法
function Dog:fetch(item)
    return self.name .. " 捡回了 " .. item
end

-- 使用
local dog = Dog.new("旺财", "柴犬")
print(dog:speak())  -- 旺财 汪汪!
print(dog:fetch("球")) -- 旺财 捡回了 球

详细用法

通用系统

-- 通用类创建函数
local function class(base)
    local cls = {}

    -- 如果有基类,设置继承
    if base then
        setmetatable(cls, { __index = base })
        cls.super = base
    end

    cls.__index = cls

    -- 通用构造函数
    function cls.new(...)
        local obj = setmetatable({}, cls)
        if cls.init then cls.init(obj, ...) end
        return obj
    end

    -- 支持创建子类
    function cls:extend()
        return class(self)
    end

    return cls
end

-- 使用
local Shape = class()

function Shape:init(name)
    self.name = name
end

function Shape:area()
    error("子类必须实现 area 方法")
end

function Shape:describe()
    return string.format("%s: 面积=%.2f", self.name, self:area())
end

-- 创建子类
local Circle = Shape:extend()

function Circle:init(radius)
    Shape.init(self, "圆形") -- 调用父类 init
    self.radius = radius
end

function Circle:area()
    return math.pi * self.radius ^ 2
end

-- 使用
local c = Circle.new(5)
print(c:describe()) -- 圆形: 面积=78.54

封装与私有性

-- 使用闭包实现私有变量
function BankAccount(initial)
    local balance = initial or 0  -- 私有变量

    local account = {}

    function account:deposit(amount)
        if amount <= 0 then error("金额必须为正数") end
        balance = balance + amount
    end

    function account:withdraw(amount)
        if amount > balance then error("余额不足") end
        balance = balance - amount
    end

    function account:getBalance()
        return balance
    end

    return account
end

-- 使用
local acc = BankAccount(100)
acc:deposit(50)
acc:withdraw(30)
print(acc:getBalance()) -- 120
-- print(acc.balance)   -- nil,无法直接访问

多重继承

-- 多重继承
local function createClass(...)
    local bases = { ... }
    local cls = {}
    cls.__index = cls

    -- 在多个基类中查找方法
    setmetatable(cls, {
        __index = function(t, k)
            for _, base in ipairs(bases) do
                local v = base[k]
                if v ~= nil then return v end
            end
        end
    })

    function cls.new(...)
        local obj = setmetatable({}, cls)
        if cls.init then cls.init(obj, ...) end
        return obj
    end

    return cls
end

-- 定义混入(Mixin)
local Serializable = {
    serialize = function(self)
        local parts = {}
        for k, v in pairs(self) do
            if type(k) == "string" and not k:match("^_") then
                table.insert(parts, k .. "=" .. tostring(v))
            end
        end
        return "{" .. table.concat(parts, ", ") .. "}"
    end
}

local Comparable = {
    __lt = function(a, b) return a:value() < b:value() end,
    __le = function(a, b) return a:value() <= b:value() end,
}

-- 使用多重继承
local Item = createClass(Serializable, Comparable)

常见场景

单例模式

-- Lua 单例实现
local Singleton = {}
Singleton.__index = Singleton

local instance = nil

function Singleton.getInstance()
    if not instance then
        instance = setmetatable({}, Singleton)
        instance:init()
    end
    return instance
end

function Singleton:init()
    self.data = {}
end

-- 使用
local s1 = Singleton.getInstance()
local s2 = Singleton.getInstance()
print(s1 == s2) -- true

观察者模式

-- 观察者模式
local Observable = {}
Observable.__index = Observable

function Observable.new()
    return setmetatable({ listeners = {} }, Observable)
end

function Observable:on(event, callback)
    if not self.listeners[event] then
        self.listeners[event] = {}
    end
    table.insert(self.listeners[event], callback)
end

function Observable:emit(event, ...)
    if self.listeners[event] then
        for _, callback in ipairs(self.listeners[event]) do
            callback(...)
        end
    end
end

function Observable:off(event, callback)
    if self.listeners[event] then
        for i, cb in ipairs(self.listeners[event]) do
            if cb == callback then
                table.remove(self.listeners[event], i)
                break
            end
        end
    end
end

-- 使用
local emitter = Observable.new()
emitter:on("data", function(data) print("收到数据: " .. data) end)
emitter:emit("data", "Hello")

注意事项

  • Lua 的 OOP 是基于原型的,不是基于的,理解 __index 的工作原理很重要
  • 使用冒号语法(:)调用方法时,self 自动传入;使用点语法(.)需要手动传入
  • 构造子时记得调用父构造函数
  • 闭包实现私有性会增加内存开销,每个实例都会创建新的函数
  • __index 链过深会影响性能,避免过深的继承层级
  • 使用 rawget 可以绕过 __index,直接访问表中存在的键

进阶用法

属性访问器

-- 使用 __index 和 __newindex 实现属性访问器
function createClassWithProps(props)
    local cls = {}
    cls.__index = function(t, k)
        -- 先在实例中查找
        local v = rawget(t, k)
        if v ~= nil then return v end
        -- 检查是否有 getter
        local prop = props[k]
        if prop and prop.get then return prop.get(t) end
        -- 在类中查找方法
        return cls[k]
    end

    cls.__newindex = function(t, k, v)
        local prop = props[k]
        if prop and prop.set then
            prop.set(t, v) -- 通过 setter 设置
        else
            rawset(t, k, v) -- 直接设置
        end
    end

    return cls
end

-- 使用
local Person = createClassWithProps({
    fullName = {
        get = function(self) return self.firstName .. " " .. self.lastName end,
        set = function(self, v)
            local parts = {}
            for w in v:gmatch("%S+") do table.insert(parts, w) end
            self.firstName = parts[1] or ""
            self.lastName = parts[2] or ""
        end
    }
})

function Person.new(firstName, lastName)
    local self = setmetatable({}, Person)
    self.firstName = firstName
    self.lastName = lastName
    return self
end

local p = Person.new("张", "三")
print(p.fullName) -- "张 三"
p.fullName = "李 四"
print(p.firstName) -- "李"