title: 在Lua中实现面向对象
date: 2024-07-01 15:56:41
tags: [Lua, 面向对象]
categories: [工作经验]

cover: https://claudia.abril.com.br/wp-content/uploads/2018/06/thinkstockphotos-97731231.jpg

0/前言

可能这就是报应吧,刚开始工作的时候没好好学Lua,平常就是把它当成了python这样的其他脚本语言来用,一直没太仔细研究。到现在要做一些比较复杂的逻辑如果还是用之前那种面向过程的思想就会很受罪,所以这篇post来记录下Lua中如何实现面向对象

1/面向对象的基础概念

学过面向对象语言的肯定都很清楚,面向对象语言有以下几大概念:

  • 对象(Object)
  • 类(Class)
  • 封装(Encapsulation)
  • 继承(Inheritance)
  • 多态(Polymorphism)

虽然lua本身没有这样的概念,但是好在它的特性比较好用且自由,我们就利用这些特性来实现

1.1/对象(Object)

众所周知Lua的数据结构基本上就只有表(table),在实际使用体验上来说,这是一把双刃剑

坏处是涉及到一些比较复杂的数据结构,要手搓的东西就比较多

好处是简单易用,基础的数据结构本质上都大差不差,直接拉一个{}出来就都能搞定。得益于表里面几乎什么东西都能放的特性,我们对表的扩展可做的事情就太多了

对象包含属性方法

那么来到Lua,用表的话与之对应的就是键值对作为表的函数值

{% note info %}
现在看来这个解释有点抽象,但是看完后面的说明与代码你就能明白了
{% endnote %}

1.2/类(Class)

面向对象里面的本质上其实就是一种抽象的工厂模式,不妨回忆一下,类的构造函数是不是就是工厂生产产品的过程呢?

所以我们在Lua中实现类的方式就是写一个工厂函数,生产出符合类这一概念的表,再通过别的方式把这个表转化成实例

1.3/封装(Encapsulation)

封装可以通过元表中的__index__newindex元方法来实现。__index用于访问未找到的表成员时的回溯操作,可以用来控制对私有成员的访问;而__newindex可以在尝试修改未定义的表成员时触发,用来限制或记录对属性的修改。

1.4/继承(Inheritance)

继承可以通过让子类的元表的__index指向父类来实现。这样,当在子类实例中查找一个方法或属性未果时,会继续在父类中查找。仔细一想好像整个继承的概念就和__index做的事情很像.

1.5/多态(Polymorphism)

多态主要通过方法重写来实现。子类可以定义与父类同名的方法,覆盖父类的行为。此外,Lua的动态特性自然支持鸭子类型,即只要对象提供了所需的方法,就可以被正确调用,无论对象属于哪个类。

在程序设计中,鸭子类型(英语:duck typing)是动态类型的一种风格。在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。这个概念的名字来源于由James Whitcomb Riley提出的鸭子测试,“鸭子测试”可以这样表述:

“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”

在鸭子类型中,关注点在于对象的行为,能作什么;而不是关注对象所属的类型。例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为"鸭子"的对象,并调用它的"走"和"叫"方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的"走"和"叫"方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。任何拥有这样的正确的"走"和"叫"方法的对象都可被函数接受的这种行为引出了以上表述,这种决定类型的方式因此得名。

(细心观察的话能发现Lua这门语言处处都是这种设计)

2/代码实现与讲解

完整实现:

-- 基础的类定义函数
function Class(base)
    local class = base or {}
    class.new = function(...)
        local instance = setmetatable({}, {__index = class})
        if instance.init then instance:init(...) end
        return instance
    end
    return class
end

-- 封装示例
local Person = Class()
function Person:init(name)
    self._name = name -- 使用下划线前缀暗示这是一个私有成员
end
function Person.getName(self)
    return self._name
end
Person.__index = Person -- 限制直接访问私有成员
Person.__newindex = function(t, k, v)
    error("Cannot directly assign to private member")
end

-- 继承与多态示例
local Student = Class(Person)
function Student:init(name, school)
    Person.init(self, name) -- 调用父类构造函数
    self.school = school
end
function Student:introduce()
    print("I am " .. self.getName(self) .. ", studying at " .. self.school)
end

-- 创建对象并演示
local alice = Person.new("Alice")
--alice._name = "Bob" -- 尝试修改私有成员会报错
print(alice.getName(alice)) -- 正确访问私有成员

local bob = Student.new("Bob", "XYZ University")
bob:introduce() -- 多态示例:Student类特有的introduce方法

2.1/创建类的工厂函数

-- 基础的类定义函数
function Class(base)
    local class = base or {}
    class.new = function(...)
        local instance = setmetatable({}, {__index = class})
        if instance.init then instance:init(...) end
        return instance
    end
    return class
end

这个函数在第一行是这样的

local class = base or {}

Class()的输入参数做了一个限定,观察后面的代码可知这是为了区分基类与子类而这样处理的,也就是说,有参数就继承父类表,否则就是全新的空表.

class.new = function(...)
        local instance = setmetatable({}, {__index = class})
        if instance.init then instance:init(...) end
        return instance
    end

创建一个新的空表instance作为对象实例,并为其设置一个元表(metatable)。元表中的__index元方法被设置为class,这意味着当在instance中找不到某个属性或方法时,Lua会去class表中查找。这种方式实现了方法和属性的继承,以及对实例访问未定义属性时的自动委托。

之后检查instance表中是否存在一个名为init的初始化方法。如果存在,则调用它,并将接收到的所有参数(通过...)传递给init方法。这一步是实例化过程中初始化对象状态的地方。如果是父类的话,自然没有init方法,所以不会执行,返回空表。

return class

最后,Class函数返回class表,这个表现在包含了创建新对象的new方法,以及可能定义的其他类级别的属性和方法,形成了一个可以用来创建对象的“类”。

2.2/对类进行封装

-- 封装示例
local Person = Class()
function Person:init(name)
    self._name = name -- 使用下划线前缀暗示这是一个私有成员
end
function Person.getName(self)
    return self._name
end
Person.__index = Person -- 限制直接访问私有成员
Person.__newindex = function(t, k, v)
    error("Cannot directly assign to private member")
end

这一部分就是具体对基类进行封装

function Person:init(name)
    self._name = name -- 使用下划线前缀暗示这是一个私有成员
end

这里实现的是构造函数,通过self来引用当前对象,self._name来引用当前对象的私有成员_nameself是Lua中的关键字,表示当前对象。定义了Person类的初始化方法init,它接收一个参数name,并将它存储为实例的私有属性_name。在Lua中,虽然没有严格的私有成员概念,但通过约定俗成,使用下划线前缀表示该成员应该是“私有的”,不应直接从外部访问或修改。

function Person.getName(self)
    return self._name
end

提供一个公共方法getName,用于安全地获取私有属性_name的值。这样,外界可以通过这个方法访问私有成员的值,而不需要直接访问私有属性。

Person.__index = Person -- 限制直接访问私有成员

这行代码设置Person类的元表的__index元方法为自身。这意味着当尝试访问Person实例中不存在的属性时,Lua会去Person类中查找。这本身不直接用于限制访问,但在结合__newindex使用时,有助于控制属性访问。

Person.__newindex = function(t, k, v)
    error("Cannot directly assign to private member")
end

设置Person类的元表的__newindex元方法。当尝试给Person的实例赋值一个不存在的键(这里通常是尝试修改私有成员)时,这个函数会被调用。在这个例子中,它抛出一个错误,阻止了对任何未明确定义的属性(特别是暗示为私有的属性)的直接赋值,从而实现了对私有成员的保护,强化了封装性。

2.3/继承与多态

-- 继承与多态示例
local Student = Class(Person)
function Student:init(name, school)
    Person.init(self, name) -- 调用父类构造函数
    self.school = school
end
function Student:introduce()
    print("I am " .. self.getName(self) .. ", studying at " .. self.school)
end

这段代码就是实际对Person类多态的实现

local Student = Class(Person)

这里通过指定Class()参数为Person,创建了一个父类为Person的子类Student,也就是说Student表会继承Person表中的所有属性与方法

function Student:init(name, school)
    Person.init(self, name) -- 调用父类构造函数
    self.school = school
end

Student类中定义一个初始化方法init。这个方法首先通过Person.init(self, name)调用了父类的初始化方法,实现了父类属性(在这个案例中是name)的初始化。然后,它为Student类添加了一个新的属性school,并赋予传入的值。这是继承的一个典型应用,即子类可以在保留父类特性的同时,扩展自己的特性。

function Student:introduce()
    print("I am " .. self.getName(self) .. ", studying at " .. self.school)
end

定义了Student类的一个新方法introduce。这个方法打印出学生的名字(通过调用继承自Person类的getName方法得到)和学校名字。这里体现了多态性:尽管getName方法是从父类继承而来,Student类通过扩展自己的方法(introduce),以特定于子类的方式使用了这个继承的方法,展示了不同的行为。这说明即使使用了同样的方法名(如getName),在不同类的上下文中可以有不同的表现,符合面向对象编程中多态的概念。

2.4/实例化与使用

-- 创建对象并演示
local alice = Person.new("Alice")
--alice._name = "Bob" -- 尝试修改私有成员会报错
print(alice.getName(alice)) -- 正确访问私有成员

local bob = Student.new("Bob", "XYZ University")
bob:introduce() -- 多态示例:Student类特有的introduce方法

这一段就是对我们刚才定义的PersonStudent类的实际使用

local alice = Person.new("Alice")

这行代码通过调用Person类的new方法创建了一个名为alicePerson对象,并将名字设为"Alice”。alice现在是一个包含了_name属性(尽管是私有的)和getName方法等的实例。

print(alice.getName(alice)) -- 正确访问私有成员

通过调用alice对象的getName方法来间接访问其私有属性_name,并打印出"Alice"。这种方法允许安全地访问和展示私有数据,而不违反封装原则。

local bob = Student.new("Bob", "XYZ University")

创建了一个名为bobStudent对象,继承了Person类的属性和方法,并且具有额外的属性school,初始化为"XYZ University"

bob:introduce() -- 多态示例:Student类特有的introduce方法

调用bob对象的introduce方法,这是Student类新增的方法,用于介绍学生自己。这个方法展示了多态性:虽然getName方法是继承自Person类的,但通过Student类的上下文(即introduce方法),它以一种特定于Student类的方式来使用,即不仅打印名字,还打印学校信息。这表明,尽管方法名相同,但通过不同类的不同实现,可以产生不同的行为,是面向对象多态性的直接体现。

一个还在寻找自己的三流开发者