Lua 面向对象
2020-10-22

0x1 Lua简介

自己去查

0x2 Lua元表

Lua 最重要的两个结构及 tablefunction ,table 可以理解为 javascript 里的 {} 结构,但是缺没法定义一个有 思想 的属性。

public string today { get { return DateTime.Now.ToString(); } }

比如我们在 .NET 里可以通过 get set 关键字定义访问器,又或者在 JS 里通过 defineProperty 来定义访问器。

Object.defineProperty(this,"today",{ get: ()=>new Date().toString()})

那么 Lua 里需要怎么定义呢?
通过元表 metatable 来实现,metatable 可以理解为具有特有属性的 table 下面罗列了部分 metatable 的私有属性。


prop desc
index 索引获取事件
newindex 索引定义事件
call 执行 {temp}() 时事件
len 执行 #{temp} 时事件

元表 metatable 也是 table 那么它的私有属性也是可以重写的,我们通过 __{prop} 来重写元表的访问事件

{__len = function(t) return -1 end }

以上是一个重写元表获取长度的demo

0x3 访问器理解

local map={};
local o={
    __index=function (t,k)
        return k;
    end
};
setmetatable(map, o);
print(map.getkeytest);
-- < getkeytest

map.getkeytest=nil;
print(map.getkeytest);
-- < getkeytest

map.getkeytest=16;
print(map.getkeytest);
-- < 16

在运行以上代码时你会发现一个问题,在 map 未设置 getkeytest 值时,它会正常的去访问 __index 索引器,但是在 getkeytest 赋值过后它响应的却是 16 ,我们在来看看 __index 事件的文档,文档上没写。实际在访问 map 的时,当你访问的 key 未定义值为空 时才会去触发元表的 __index 方法。

local map={ getkeytest=16 };
local o={
    __index=function (t,k)
        return k;
    end,
    __newindex=function (t,k,v )
        print("__newindex",k)
    end
};
setmetatable(map, o);

print(map.getkeytest);
-- < 16

map.getkeytest=18;
print(map.getkeytest);
-- < 18

map.getkeytest=nil;
map.getkeytest=19;
-- < __newindex    getkeytest

print(map.getkeytest);
-- < getkeytest 

map.getotherkeytest=20;
-- < __newindex    getotherkeytest
print(map.getotherkeytest);
-- < getotherkeytest  

我们在来试试 __newindex 同样,我们可以看出当 map 存在 getkeytest

  • 访问 getkeytest 不会触发 __index
  • getkeytest 赋值,且值不为空时 不会触发 __newindex
  • getkeytest 赋值为 nil不会触发 __newindex
  • 对以为 nilgetkeytest 赋值时 触发了 __newindex
  • 再访问 getkeytest 触发了 __index
  • getotherkeytest 赋值 触发了 __index

0x4 面向对象思路

在面向对象之前,我们在说下静态类一般类的实现思路,我不喜欢 访问器,如果你还不知道 : 访问器,那么我觉得你可以不知道更好
静态类就非常简单了很容易实现,如

local staic_class_demo={}
do
    staic_class_demo.print=function(arg)
        print("staic_class_demo",arg);
    end
end
-- 如果是独立文件 需要返回 staic_class_demo
-- return staic_class_demo
staic_class_demo.print("你好")
-- < staic_class_demo    你好

接下来,面向对象

local new_obj = function(args)
    local map, o, meta = {}, {}, {}
    local list = {}   
    setmetatable(map, o)
    o.__index = function(t, key)       
        if (meta[key]) then
            if (meta[key].get) then
                return meta[key].get()
            else
                return nil
            end
        else
            return o[key]
        end
    end
    o.__newindex = function(t, key, v)
        if (meta[key]) then
            if (meta[key].set) then
                meta[key].set(v)
            else
                return
            end
        else
            o[key] = v
        end
    end
    map.add = function(a)
        table.insert(list, a)
    end
    meta.length = {
        get = function()
            return #list
        end
    }
    meta.name = {
        get = function()
            return args
        end
    }   
    return map;
end

local obj1=new_obj("obj1")
local obj2=new_obj("obj2")
obj1.add(1);
obj1.add(2);

obj2.add(3);
obj2.add(4);
obj2.add(5);

print(obj1.name,obj1.length)
-- < obj1    2
print(obj2.name,obj2.length)
-- < obj2    3

继续测试

obj1.name="test1";
obj2.name="test1";
print(obj1.name,obj1.length)
-- < obj1    2
print(obj2.name,obj2.length)
-- < obj2    3

这里需要注意的几个点是

  • 元表 o 只是用来定义 _index_newindex
  • 需要通过 meta 来定义特殊字段,自定义访问器
  • 最后 map 关联元表然后返回
  • 如果直接将普通字段注册到 map 必须最后设置元表 setmetatable(map, o) 否则会死循环 stack overflow

0x5 快速定义封装

了解了以上内容后我们可以封装一个快速定义自有属性的方法 Object.defineProperty 类似 JS 对象的定义属性

--
-- Object.lua
--
-- use for defineProperty
--
-- {
--     key = {
--         get = function()
--             --get function
--         end,
--         set = function(value)
--             -- set function
--         end
--     }
-- }

local Object = {_version = "1.1"}
do
    Object.defineProperty = function(map, props)
        local o = {}
        setmetatable(map, o)

        o.__index = function(t, k)
            local tar = props[k]
            if (tar) then
                if (tar.value) then
                    return tar.value
                elseif (tar.get) then
                    return tar.get()
                else
                    return nil
                end
            else
                return o[k]
            end
        end
        o.__newindex = function(t, k, v)
            local tar = props[k]
            if (tar) then
                if (tar.set) then
                    tar.set(v)
                end
            else
                o[k] = v
            end
        end
    end
    -- end defineProperty
end
return Object

0x6 使用方法

local new_obj = function(args)
    local map, list = {}, {}
    local like = nil
    map.add = function(a)
        table.insert(list, a)
    end
    -- 定义特殊属性 通过实现get/set来实现
    Object.defineProperty(
        map,
        {
            -- 定义长度属性,只读
            length = {
                get = function()
                    return #list
                end
            },
            -- 定义名称属性,只读
            name = {
                get = function()
                    return args.."\t"..like
                end
            },
            -- 定义喜好属性,可读可写
            like={
                get=function ()
                    return like
                end,
                set=function (v)
                    like=v
                end
            }
        }
    )
    return map
end

local obj3=new_obj("obj3")
obj3.like="is me"
print(obj3.like)
-- < is me
print(obj3.name)
-- < obj3    is me

0x7 混合类

local RSS = {_Verison = 5.3, description = "类实现"}
do
    local Object = require("object")

    local strlen = function(str)
        return #str
    end

    RSS.echo = function(...)
        local text = table.concat({...}, "\t")
        print(text)
    end

    RSS.new = function(name)
        local map = {}
        local age = 0
        map.say = function()
            RSS.echo(map.name, strlen(map.name), age)
        end
        Object.defineProperty(
            map,
            {
                name = {value = name},
                age = {
                    get = function()
                        return age
                    end,
                    set = function(v)
                        age = v
                    end
                }
            }
        )
        return map
    end
end
-- return RSS
local jim = RSS.new("jim")
jim.name="jim2"
jim.age = 8
local jerry = RSS.new("jerry")
jerry.name="jerr2"
jerry.age = 24
jim.say()
-- < jim    3    8
jerry.say()
-- < jerry    5    24
RSS.echo("abc","bbc")
-- < abc    bbc

0x8 其他扩展

扩展一个 echo 方法,需要 require("JSON")

local echo = function(...)
    local temp = {}
    for k, value in ipairs({...}) do
        if ("table" == type(value)) then
            table.insert(temp, json.encode(value))
        else
            table.insert(temp, value)
        end
    end
    print(table.concat(temp, "\t"))
end

0x9 总结

可以发现我们并没有常规方法来实现对象(你可以查阅参考文档来了解怎么使用常规方法定义对象),通过方法来定义对象的好处是整个语法都非常一致,用 . 来访问对象的方法及属性而不是 : ,然后在属性定义上也非常友好,不需要使用到 self不要去查阅什么是self,以上 lua 面向对象,完毕。

0xA 参考文档