Lua 实现 class类

Lua没有类这一说,本身是不能像C++那样直接实现继承,但我们可以用万能的table表来接近实现类的功能,实现面向对象

类的实现由两种,一种是纯copy所有的域,还有一种是元表访问,只有修改时才会在当前类真的添加父类的键,而不是修改父类的,这里用的是元表访问

一、Lua 实现 class 的基本方式

众所周知,lua 可以通过元表来实现一些骚操作,目前主流的 class 实现也是通过元表来实现的。

local t = {}
local mt = {
    __index = {
        a = 10,
        b = 20,
        add = function(a, b)
            return a + b
        end
    }
}
setmetatable(t, mt)
print(string.format("%d + %d = %d", t.a, t.b, t.add(t.a, t.b)))
-- 10 + 20 = 30

通过元表 mt 的 index 字段可以让表 t 获得一些本身没有的字段, 通过这样的一个形式,我们就可以达到从实例中调用类的方法。另外,__index 也可以是方法,这里只简单介绍使用的关键部分,有兴趣的可以看下之前整理的元表、元方法的链接。

https://blog.csdn.net/weixin_43284350/article/details/118338941?spm=1001.2014.3001.5502

通过元表就可以简单的实现一个 class 函数,用来作为类的声明了:

local Object = {
    class_id = 0,
    add = function (a,b)       
        print("a+b=",a+b)
    end
}
print(Object)
function Object:new(o)
    o = o or {}
    print(self)
    setmetatable(o,self) -- 对象o调用不存在的成员时都会去self中查找,而这里的self指的就是Object
    self.__index = self
    return o
end
-- 创建对象测试
local o1 = Object:new()
o1.add(1,1)              --调用父类函数

在这里插入图片描述

以上就是一个简单的 class 实现,通过 metatable 的嵌套设置, 可以保证实例一定可以访问到含有同名函数的最近一个父类的方法。

有了以上的基础,我们可以把构建类的方法包装:

local function class(className, super) -- className 是类名,super 为父类(可为空)
    -- 构建类
    local clazz = { __cname = className, super = super }
    if super then
        -- 设置类的元表,此类中没有的,可以查找父类是否含有
        setmetatable(clazz, { __index = super })
    end
    -- new 方法创建类对象
    clazz.new = function(...)
        -- 构造一个对象
        local instance = {}
        -- 设置对象的元表为当前类,这样,对象就可以调用当前类生命的方法了
        setmetatable(instance, { __index = clazz })
        if clazz.ctor then
            clazz.ctor(instance, ...)
        end
        return instance
    end
    return clazz
end

下面为测试:

-- 声明 犬类
local Dog = class("Dog")
Dog.static = 'I am a Dog'

-- 构造函数
function Dog:ctor(breed, color)
    self.breed = breed or ""
    self.color = color or ""
end

function Dog:bark()
    print("汪!汪!汪!")
end

function Dog:printSelf()
    print("品种:",self.breed)
    print("颜色:",self.color)
end

-- 声明Bulldog(斗牛犬) 并且继承 Dog
local Bulldog  = class("Bulldog", Dog)
function Bulldog:ctor(...)
    self.super.ctor(self, ...)
end

-- 重写父类的方法
function Bulldog:bark()	
    print("嗷呜!嗷呜!")
end

-- 声明 DdrDog(德牧), 并且继承 Dog
local DdrDog = class("DdrDog", Dog)
DdrDog.static = 'I am a DdrDog'		-- 重写父类的常量

local bulldog1 = Bulldog.new("斗牛犬","黑色")
local ddrdog1 = DdrDog.new("德国牧羊犬","白色")

print(bulldog1.static)
bulldog1:printSelf()
bulldog1:bark()

print(ddrdog1.static)
ddrdog1:printSelf()
ddrdog1:bark()

print("狗类的self:",Dog)
print("斗牛犬的super:",bulldog1.super)
print("斗牛犬的self:",bulldog1)

Lua 实现 class类_第1张图片

二、cocos2dx-lua 中的 class 实现

我们项目用的就是这种方面在Lua实现的类,不过在项目中重写了 class(classname, super)函数:

-- @param string classname 类名
-- @param [mixed super] 父类或者创建对象实例的函数
function class(classname, super)
    local superType = type(super)
    local cls

    if superType ~= "function" and superType ~= "table" then
        superType = nil
        super = nil
    end

    if superType == "function" or (super and super.__ctype == 1) then
        -- inherited from native C++ Object
        cls = {}

        if superType == "table" then
            -- copy fields from super
            for k,v in pairs(super) do cls[k] = v end
            cls.__create = super.__create
            cls.super    = super
        else
            cls.__create = super
            cls.ctor = function() end
        end

        cls.__cname = classname
        cls.__ctype = 1

        function cls.new(...)
            local instance = cls.__create(...)
            -- copy fields from class to native object
            local peer = tolua.getpeer(instance)
            if not peer then
                peer = {}
                tolua.setpeer(instance, peer)
            end
            for k,v in pairs(cls) do peer[k] = v end
            peer.class = cls
            instance:ctor(...)
            return instance
        end

    else
        -- inherited from Lua Object
        if super then
            cls = {}
            setmetatable(cls, {__index = super})
            cls.super = super
        else
            cls = {ctor = function() end}
        end

        cls.__cname = classname
        cls.__ctype = 2 -- lua
        cls.__index = cls

        function cls.new(...)
            local instance = setmetatable({}, cls)
            instance.class = cls
            instance:ctor(...)
            return instance
        end
    end

    return cls
end

以下是项目class的说明文档,(PS:项目的的大佬是真的多,我这小菜鸡只能仰望着)

--[[--

创建一个类

~~~ lua

-- 定义名为 Shape 的基础类
local Shape = class("Shape")

-- ctor() 是类的构造函数,在调用 Shape.new() 创建 Shape 对象实例时会自动执行
function Shape:ctor(shapeName)
    self.shapeName = shapeName
    printf("Shape:ctor(%s)", self.shapeName)
end

-- 为 Shape 定义个名为 draw() 的方法
function Shape:draw()
    printf("draw %s", self.shapeName)
end

--

-- Circle 是 Shape 的继承类
local Circle = class("Circle", Shape)

function Circle:ctor()
    -- 如果继承类覆盖了 ctor() 构造函数,那么必须手动调用父类构造函数
    -- 类名.super 可以访问指定类的父类
    Circle.super.ctor(self, "circle")
    self.radius = 100
end

function Circle:setRadius(radius)
    self.radius = radius
end

-- 覆盖父类的同名方法
function Circle:draw()
    printf("draw %s, raidus = %0.2f", self.shapeName, self.raidus)
end

--

local Rectangle = class("Rectangle", Shape)

function Rectangle:ctor()
    Rectangle.super.ctor(self, "rectangle")
end

--

local circle = Circle.new()             -- 输出: Shape:ctor(circle)
circle:setRaidus(200)
circle:draw()                           -- 输出: draw circle, radius = 200.00

local rectangle = Rectangle.new()       -- 输出: Shape:ctor(rectangle)
rectangle:draw()                        -- 输出: draw rectangle

~~~

### 高级用法

class() 除了定义纯 Lua 类之外,还可以从 C++ 对象继承类。

比如需要创建一个工具栏,并在添加按钮时自动排列已有的按钮,那么我们可以使用如下的代码:

~~~ lua

-- 从 cc.Node 对象派生 Toolbar 类,该类具有 cc.Node 的所有属性和行为
local Toolbar = class("Toolbar", function()
    return display.newNode() -- 返回一个 cc.Node 对象
end)

-- 构造函数
function Toolbar:ctor()
    self.buttons = {} -- 用一个 table 来记录所有的按钮
end

-- 添加一个按钮,并且自动设置按钮位置
function Toolbar:addButton(button)
    -- 将按钮对象加入 table
    self.buttons[#self.buttons + 1] = button

    -- 添加按钮对象到 cc.Node 中,以便显示该按钮
    -- 因为 Toolbar 是从 cc.Node 继承的,所以可以使用 addChild() 方法
    self:addChild(button)

    -- 按照按钮数量,调整所有按钮的位置
    local x = 0
    for _, button in ipairs(self.buttons) do
        button:setPosition(x, 0)
        -- 依次排列按钮,每个按钮之间间隔 10 点
        x = x + button:getContentSize().width + 10
    end
end

~~~

class() 的这种用法让我们可以在 C++ 对象基础上任意扩展行为。

既然是继承,自然就可以覆盖 C++ 对象的方法:

~~~ lua

function Toolbar:setPosition(x, y)
    -- 由于在 Toolbar 继承类中覆盖了 cc.Node 对象的 setPosition() 方法
    -- 所以我们要用以下形式才能调用到 cc.Node 原本的 setPosition() 方法
    getmetatable(self).setPosition(self, x, y)

    printf("x = %0.2f, y = %0.2f", x, y)
end

~~~

**注意:** Lua 继承类覆盖的方法并不能从 C++ 调用到。也就是说通过 C++ 代码调用这个 cc.Node 对象的 setPosition() 方法时,并不会执行我们在 Lua 中定义的 Toolbar:setPosition() 方法。
]]

三、模块与包

模块类似于一个封装库,从 Lua 5.1 开始,Lua 加入了标准的模块管理机制,可以把一些公用的代码放在一个文件里,以 API 接口的形式在其他地方调用,有利于代码的重用和降低代码耦合度。

Lua 的模块是由变量、函数等已知元素组成的 table,因此创建一个模块很简单,就是创建一个 table,然后把需要导出的常量、函数放入其中,最后返回这个 table 就行。以下为创建自定义模块 module.lua,文件代码格式如下:

-- 文件名为 module.lua
-- 定义一个名为 class 的模块
class = {}
 
-- 定义一个常量
class.constant = "这是一个常量"
 
-- 定义一个函数
function class.func1()
    io.write("这是一个公有函数!\n")
end
local function func2()
    print("这是一个私有函数!")
end
 
function class.func3()
    func2()
end
 
return class

require 函数

require()也可以看出是导入和引用类似C的include()C#的using ,java的Import

require("<文件名>")
require "<文件名>"

执行 require 后会返回一个由模块常量或函数组成的 table,并且还会定义一个包含该 table 的全局变量。

reqmodule.lua

-- reqmodule.lua 文件
-- module 模块为上文提到到 module.lua
require("module")
 
print(class.constant)
class.func3()

在这里插入图片描述

四、导入类和调用类

注意:
一个模板在一个文件里,一个模块可看成是个表,表可以写成类的形式
本文的类实际是一个table类型的表,这里是一个类用一个单独的lua文件来存放

没有return的模块情况:

外界获取后可以直接通过.来修改表里的字段,但是如果是在方法里则不能修改添加表里的字段,但是可以在方法里用return来接收表里的变量

1.新建一个文件 class.lua,用来写objA

obj = {MyTest2 = 1} -- MyTest2可以通过表名.获取
new = 0 -- 不是obj表里的字段
MyTest = 0
obj.test = 666 -- 是obj表里的字段,可以在表外直接定义赋值修改
function obj.Play()
    print("obj.play")
end
function obj.Sum(num1,num2)
    return num1+num2
end
function obj.ADD(qwq)
    MyTest = qwq --外界可以用方法赋值,但是无法通过表获取,需要用return返回
    return MyTest
end 

2.新建一个文件test.lua,外界获取的全局类,可以直接用类名.调用类的方法

--这里因为没写return,可以直接用那个ClassA模块的表名点出来使用
require("class")
obj.Play()
print(obj.Sum(10,20))
obj.new = 10
print(obj.new)      --10看起来可以直接在外部修改,但实际并不是对模块的new操作,而是在表里新添一个字段
print(obj.MyTest)   --无法得到模块里的值Mytest,打印nil
print(obj.ADD(123)) --但是可以通过返回值得到

Lua 实现 class类_第2张图片

局部类用return返回,并且需要接收:

新建一个文件ClassB ,objB是局部变量
此时可以调用,因为objA是全局变量,可以在外部调用,如果定义local类型的,那么只对当前文件有效,外部无法调用,如果外部需要使用呢?

------------------------- class.lua
local objB = {}
function objB.Play()
    print("objB.play")
end

function objB.Sum(num1,num2)
    return num1+num2
end
return objB
------------------------- test.lua

local objB = require("class")--获取class文件模块
objB.Play() 
print(objB.Sum(100,200))

在这里插入图片描述

五、self关键字

self对应c#里的this,c#中关键字可以省略,lua中self不能省略
self使用this作为当前对象

self. xx 就是当前表的xx ,如果有这个变量,就获取或者修改 ,如果没有这个值,则在表里新建变量

看使用会比较直观:

-------------------------------------class.lua
local objC = {name = "Tom",age = 18}
function objC.SetName(self,_name)
    self.name = _name
end
function objC.GetName(self)
    print(self.name)
end
return objC
-------------------------------------test.lua
local obj = require("class")
obj.GetName(obj) --这里获取的时候要传入当前的变量objC,从而找到class模块(表)里的字段
obj.SetName(obj,"Jack")
obj.GetName(obj) 
obj.GetName()       --不传本对象,会报错

Lua 实现 class类_第3张图片

文件内部表里的函数只有传入self,并在内部写了self.xxx ,外界变量获取后传入自己变量,才能找到表里的字段,进行修改读取操作
如果不在函数参数写self,不传入当前表,函数就无法获取到自己表里的值。也就无法在表里进行修改、添加操作。

六、构造函数new 也就是 : (冒号)

冒号的作用就是:定义函数时,给函数的添加隐藏的第一个参数self;
调用函数时,默认把当前调用者作为第一个参数传递进去
如果使用.需要传递self,如果使用:不需要传递self

-------------------------------------class.lua
local ObjD = {name="null",age = 0}
--构造函数
function ObjD:New(_name,_age)
    self.name = _name
    self.age = _age
end
function ObjD:Play()
    print(self.name)
    print(self.age)
end
return ObjD
-------------------------------------test.lua
local objD = require("class")
objD:New("Ls",1) --修改表classD文件下的表里字段
objD:Play() --打印出classD里的字段 --Ls 1

在这里插入图片描述

总结

因为lua没有类这一说,所以我们用表来代替类(因为表可以写成数组,字典,可以写表方法(表库)),把表写成类的形状
因为类有字段,所以我们就在表上加字段,并且赋值。(使用字典的形式)
因为类有方法,所以我们就用fucntion 表名.xxx 来定义表的方法,元表也可以定义元方法
因为类有构造函数,所以我们用冒号:来区分,不过这里的冒号只是表示说默认第一个方法参数是self,使用self可以让表通过调用表方法给表里 赋值、修改、添加字段的作用
因为类可以被继承,所以我们就用setmetatable()来给2个表设置子类(表)父类(元表)(使用元表)
子类表继承了父类的字段,但是没有父类的方法,不过却可以调用父类的方法。
子类也可以继续定义扩展方法,甚至可以覆盖父类的方法
参考链接:https://blog.csdn.net/liaoshengg/article/details/81381423

你可能感兴趣的:(Lua学习笔记,lua,类,继承)