Lua——Lua中的面向对象

本文介绍在Lua中实现面向对象的多种方法,包括使用Metatable和Metamethod的基础方案,以及改进后的Class和Instance分离方案,还有高效但内存消耗大的Clone方案,最后提出了一种结合Metatable和Clone的折中方案。

开始

Lua本身并不是面向对象的语言、不存在类的概念。Lua官网16.1 – Classes中有如下描述。

Lua does not have the concept of class

但我们可以在Lua中来实现面向对象。在Lua中实现面向对象的方式有很多种,本篇挑比较常见的几种方案进行解析。

Metatable和Metamethod——官方的方案

首先明确一点:表(Table)是Lua中的基本数据类型之一,也是Lua中唯一的数据结构。我们将要实现的类本质上也是一张表。Lua官网11 – Data Structures

Tables in Lua are not a data structure; they are the data structure. All structures that other languages offer—arrays, records, lists, queues, sets—are represented with tables in Lua. More to the point, tables implement all these structures efficiently.

之后再来看看Metatable和Metamethod。官方的解释可以参考Lua官网13 – Metatables and Metamethods

其中下面这句话为面向对象提供了思路。

Any table can be the metatable of any other table; a group of related tables may share a common metatable (which describes their common behavior); a table can be its own metatable (so that it describes its own individual behavior). Any configuration is valid.

看看官方在这种思路下实现的Account类:

Account = {balance = 0}

function Account:withdraw (v)
	self.balance = self.balance - v
end

function Account:deposit (v)
	self.balance = self.balance + v
end

function Account:new (o)
	o = o or {}   -- create object if user does not provide one
	setmetatable(o, self)
	self.__index = self
	return o
end

Account类提供了new方法,用来创建一个Account类的对象。

方法内部先构建一张表o,并将其Metatable和元方法(Metamethod)__index均设置为self。这里将__index设置为一张表是Lua语法上的便利,它的本质还是一个函数(Method),具体参照Lua官网。

当我们调用a = Account:new()创建一个新Account对象时,new方法里的self就指向Account。也就是说,现在a的Metatable和__index元方法均指向Account,那么就实现了一个简单的继承。

接下来调用a:deposit(100),Lua在表a中找不到deposit,于是会先看它有没有Metatable,如果有,再看它的Metatable有没有__index元方法或__index对应的表。这二者缺一不可。

最后的结果当然是查到了deposit方法,于是代码顺利执行,通过打印a.balance可以看见此时a.balance已经为100了。

官方还提供了继承的实现方案来实现从Account派生出SpecialAccount类。

SpecialAccount = Account:new()

个人认为,官网上这种方案实现的继承并不优雅。用惯了面向对象的语言,看见此段代码的第一反应是:SpecialAccountAccount类的一个对象,而不是一个派生类。

对于常在面向对象语言和Lua之间切换的人,这很容易令人困惑。因此,下面不再对官方的例子进行深入剖析。而是利用官方的思路来重新进行面向对象的设计。

Metatable和Metamethod——改进方案

在面向对象语言中,类(Class)和实例(Instance)是不同的概念。因此,在Lua中实现面向对象,我也会将Class和Instance分开。

完整的代码如下:

--MetatableClass.lua
local Class = {}
--类名,保存“类名-类”记录
local tableClassNames = {}

local function New(className, super)
    assert(type(className) == 'string' and #className > 0)
    assert(tableClassNames[className] == nil, 'Try to redefine a class with name : [' .. className .. ']')
    local tableNewClass = {}
    tableNewClass.className = className
    tableNewClass.super = super
    tableNewClass.category = 'Class'
    tableNewClass.Ctor = false
    tableNewClass.Dtor = false
    tableNewClass.__index = super
    setmetatable(tableNewClass, tableNewClass)

    --创建新对象
    tableNewClass.New = function(...)
        local tableNewObject = {
            type = tableNewClass,
            category = 'Instance'
        }
        
        tableNewObject.__index = tableNewClass
        setmetatable(tableNewObject, tableNewObject)
        
        do
            --递归调用父类的构造函数
            local Create
            Create = function(class, ...)
                if type(class.super) == 'table' then Create(class.super, ...) end
                if type(class.Ctor) == 'function' then class.Ctor(tableNewObject, ...) end
            end
            Create(tableNewClass, ...)
        end

        --销毁对象
        tableNewObject.Dispose = function(self)
            --依次调用所有父类的析构函数
            local currentClass = self.type
            while currentClass ~= nil do
                if type(currentClass.Dtor) == 'function' then currentClass.Dtor(self) end
                currentClass = currentClass.super
            end
        end
        return tableNewObject
    end
    tableClassNames[className] = tableNewClass
    return tableNewClass
end

Class.New = New
return Class

简单讲解下代码:

  1. 通过Class.New方法来创建一个类,为保证全局的唯一性,需要对每个类提供唯一的名字;同时可以指定该类的父类以实现继承。
  2. 通过newClass.New方法来创建一个newClass的实例,创建该实例时,将会依次递归调用基类的构造函数(Ctor)。
  3. 通过newClassInstance.Dispose方法可以销毁newClassInstance实例,销毁该实例时,将会依次递推调用基类的析构函数(Dtor)。
  4. 代码中,为类和实例增加了标识性字段category,用来区分一个对象是类还是实例。

接下来看看如何使用:

Class = require("MetatableClass")

ClassA = Class.New('ClassA')

ClassA.Ctor = function(self)
    print 'Ctor in ClassA'
end

ClassA.Dtor = function(self)
    print 'Dtor in ClassA'
end

ClassA.Print = function(self)
    print 'Print in ClassA'
end

ClassB = Class.New('ClassB', ClassA)

ClassB.Ctor = function(self)
    print 'Ctor in ClassB'
end

ClassB.Dtor = function(self)
    print 'Dtor in ClassB'
end

b = ClassB.New()
b.Print()
b:Dispose()

最后输出如下:
在这里插入图片描述

这是一种简单、易懂的方案,可以应付大部分场合。但是,当继承关系过深,逐层去索引将会带来效率问题。这个问题也是接下来要解决的问题。

Clone——粗暴的方案

为了避免逐层索引带来的效率问题,这里提供了Clone的方案来解决此问题。

与上一种方案不同,假设有一个类ClassA,当需要从ClassA创建实例时,我们可以将ClassA的所有字段通通复制进一张新表(新实例)。可以通过如下的Clone方法来完成此操作:

local function Clone(object)
    local temp = {}

    local function CloneProcess(innerObject)
        if type(innerObject) ~= 'table' then
            return innerObject
        elseif temp[innerObject] ~= nil then
            return temp[innerObject]
        end

        local newObject = {}
        temp[innerObject] = newObject
        for k, v in pairs(innerObject) do
            newObject[CloneProcess(k)] = CloneProcess(v)
        end
        return setmetatable(newObject, getmetatable(innerObject))
    end

    return CloneProcess(object)
end

这种方案简单、粗暴,不存在逐层查找的问题,但是会带来新的问题,即每一个实例都会拥有类的完整的字段集,从而造成内存的浪费。我通常会在不需要创建大量实例时使用该方案。

为了折中解决以上两种方案带来的问题,下面提供另一种实现方案。

Metatable+Clone——折中的方案

这种方案来自云风的博客,原文可以在云风的BLOG中可以找到。

贴上原始代码:

local _class={}
 
function class(super)
	local class_type={}
	class_type.ctor=false
	class_type.super=super
	class_type.new=function(...) 
			local obj={}
			do
				local create
				create = function(c,...)
					if c.super then
						create(c.super,...)
					end
					if c.ctor then
						c.ctor(obj,...)
					end
				end
 
				create(class_type,...)
			end
			setmetatable(obj,{ __index=_class[class_type] })
			return obj
		end
	local vtbl={}
	_class[class_type]=vtbl
 
	setmetatable(class_type,{__newindex=
		function(t,k,v)
			vtbl[k]=v
		end
	})
 
	if super then
		setmetatable(vtbl,{__index=
			function(t,k)
				local ret=_class[super][k]
				vtbl[k]=ret
				return ret
			end
		})
	end
 
	return class_type
end

代码的基本思想是:

  1. 通过全局的class方法创建一个类。
  2. 为每一个类创建一个基础表(class_type)用来存放一些基础的字段和方法,同时创建一个配套的虚表(vtbl),用来存储继承来的字段和方法,并为class_typevtbl构建索引关系。
  3. 通过类的new方法可以创建一个实例。在创建实例时递归调用父类的构造函数(ctor)。
  4. 改写实例的__index行为,使对实例的索引转向对class_type对应的vtbl的索引。
  5. 改写class_type__newindex来拦截对class_type的赋值,将值赋给class_type对应的vtbl
  6. 改写vtbl__index行为,当vtbl中不存在目标字段时,尝试从父类(super)的vtbl_super中索引,并将索引结果存放复制到vtbl,这样下一次再索引相同字段时,就无需再从super中索引了。

这种方案很好地折中了以上两个问题:效率和内存问题,并且提供了实现面向对象的另一种思路。

总结

上述的几种方案是常用的方案,项目中往往需要根据具体的需求采用特殊的设计。比如有些类字段不应该被修改(如类名、父类),就可以通过修改__newindex行为来阻止对这些字段的修改。

上述几个方案只是起个抛砖引玉的作用,如果有看客有其他好的实现方式,欢迎留言探讨。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值