美文网首页
深度理解Lua中的table元方法

深度理解Lua中的table元方法

作者: shy0218 | 来源:发表于2019-01-15 18:02 被阅读20次

    Lua的table是个很有意思的东西。有些内容平时写代码的时候很少接触到,但是了解一下还是很有意思的。

    这篇blog参考MetatableEvents,一个一个边写测试边细说。

    __newindex

    原文翻译:

    __newindex用于分配属性,当调用 myTable[key]=value时,如果元表中有__newindex并且指向一个function,就会调用这个function,传入的参数为table, key 和 value

    • rawset(myTable, key, value)可以跳过这个元方法直接给myTable的key属性赋值为value。
    • 如果__newindex指向的方法中,没有调用rawset方法,传入的键值对(key/value)就不会添加到myTable中。

    测试代码:

    local meta = {
        __newindex = function(t, key, value)
            print("call __newindex",t, key, value)
        end
    }
    
    local test = {}
    setmetatable(test, meta)
    
    print("test", test)
    print("meta", meta)
    
    test.name = "t1"
    test.name = "t2"
    print("test.name", test.name)
    
    ---- result output ----
    test    table: 0x7f9c13406f00
    meta    table: 0x7f9c13407240
    call __newindex table: 0x7f9c13406f00   name    t1
    call __newindex table: 0x7f9c13406f00   name    t2
    test.name   nil
    

    测试代码中,当给t的name的赋值时,就会触发元表中的__newindex指向的function,打印的信息可以看到key和value的值。

    __newindex方法中传进来的参数t的指针和test的指针指向同一个地址,说明__newindex中的参数t,并不是元表。

    测试代码中对t.name连续赋值时,__newindex会连续调用,需要留意一下这里,后面的测试会跟这里做一个对比。

    赋值之后打印 t.name 的值是空的。原因是__newindex并没有给t.name赋值,我们用一个错误的方式给t.name赋值,来加深__newindex的理解。修改一下测试代码:

    local meta = {
        __newindex = function(t, key, value)
            print("call __newindex",t, key, value)
            t[key] = value
        end
    }
    
    local test = {}
    setmetatable(test, meta)
    
    print("test", test)
    print("meta", meta)
    
    test.name = "t1"
    test.name = "t2"
    print(test.name)
    
    ---- result output ----
    ...
    lua: C stack overflow
    ...
    

    报错信息,栈溢出。因为t[key] = value这段代码会调用t元表中的__newindex的方法,__newindex的方法又会调用t[key] = value,这样就进入了死循环,导致栈溢出。这时就需要用到方法rawset

    修改测试代码:

    local meta = {
        __newindex = function(t, key, value)
            print("call __newindex",t, key, value)
            rawset(t, key, value)
        end
    }
    
    local test = {}
    setmetatable(test, meta)
    
    test.name = "t1"
    test.name = "t2"
    print("test.name", test.name)
    
    ---- result output ----
    call __newindex table: 0x7fdade404e20   name    t1
    test.name   t2
    

    这段代码中信息比较多

    __newindex中使用了rawset方法,可以看到,没有栈溢出的错误了,说明用rawset给table赋值,不会进入__newindex
    的方法。

    给t.name连续赋值,会发现只进入__newindex一次,跟之前不同的是,我们在__newindex给t.name赋了值。如果t中没有这个key时,才会进入__newindex方法。否则不会进入。

    __newindex的默认值就是上面meta.__newindex的代码。如果不需要额外处理,完全可以不写。如下:

    local meta = {}
    local test = {}
    setmetatable(test, meta)
    
    test.name = "t1"
    print("test.name", test.name)
    
    ---- result output ----
    test.name   t1
    
    

    __index

    翻译原文

    __index用于控制属性(prototype)的继承,当访问 myTable[key] 时,如果myTable中不存在这个key,但是如果元表(metatable)中有 __index时:

    • 如果__index是一个function,传递的参数是tablekey,function的返回值作为结果返回。
    • 如果__index是一个table,就返回这个表中key对应的值。
      • 如果这个table不存在该key,但是这个table有元表,会继续寻找元表中的__index属性,以此类推。都没有就返回nil
    • 使用 "rawget(myTable,key)" 可以跳过这个元方法(__index).

    写点测试:

    local test = {}
    
    local meta = {
        __index = function(t, k)
            print("__index", k)
            if rawget(t, k) == nil then
                print("Can't find ".. k)
            end
    
            return rawget(t, k)
        end,
    }
    
    setmetatable(test, meta)
    
    print("test.name1", test.name)
    test.name = "hello"
    print("test.name2", test.name)
    
    
    ---- result output ----
    
    __index name
    Can't find name
    test.name1  nil
    test.name2  hello
    

    __newindex__index其实可以类比成setter和getter,这么类比会比较容易理解,但是实际上还是有比较大的区别。

    上面的测试中__index是个function。当执行test.name时,如果test.name是nil,会调用__index的function,并返回function的返回值。否则,直接返回test[key],不会进入__index

    再做一个测试,这次__index是个table

    local test = {}
    
    local meta = {
        __index = {name="meta"},
    }
    
    setmetatable(test, meta)
    
    print("test.name1", test.name)
    test.name = "hello"
    print("test.name2", test.name)
    
    ---- result output ----
    
    test.name1  meta
    test.name2  hello
    

    这个测试可以看到,访问顺序是先访问test的name,如果没有值,再访问test元表中__index的table。如果test的元表还有元表,会继续向上访问,Lua继承的实现就是利用这个特性。

    掌握__newindex__index这两个元方法,可以把这两个元方法看做两个事件,那要就要清楚两个方法的触发条件和特性。才能融会贯通。

    举个例子:

    禁用全局变量

    local meta = {
        __newindex = function(t, k, v)
            print("Error! Can't set globle variable", k)
        end,
    
        -- 默认实现
        -- __index = function(t, k)
        --     return rawget(t, k)
        -- end
    }
    
    setmetatable(_G, meta)
    
    test = "test"
    print(test)
    
    ---- result output ----
    
    Error! Can't set globle variable    test
    nil
    

    __mode

    原文翻译:

    控制弱引用,用字符kv来代表table的是否是弱引用。这个感觉没什么好说的,只写个测试就好了。

    local meta = {__mode = "k"}
    local test = {}
    setmetatable(test, meta)
    key = {}
    test[key] = 1
    key = {}
    test[key] = 2
    for k,v in pairs(test) do
        print(v)
    end
    
    collectgarbage()
    print("collectgarbage")
    
    for k,v in pairs(test) do
        print(v)
    end
    
    ---- result output ----
    
    1
    2
    collectgarbage
    2
    

    例子中当调用collectgarbage()进行回收后,test表中只剩下一个值。弱引用的key被清理了。我们也可以在__mode中设置v,kv来表示 键和值都是弱引用。

    __call

    原文翻译:

    把table当做一个function使用,当table后跟一个圆括号时,而且table的元表中的__call指向一个function,就会调用这个function,table自己做为第一个参数,后面可接任意数量的参数,返回值就是function的返回值。

    测试代码来模拟实现一个构造方法。

    local meta = {
        __call = function(t, ...)
    
            local instance = {}
            for k, v in pairs(t) do
                instance[k] = v
            end
            return instance
        end
    }
    
    local A = setmetatable({}, meta)
    
    function A:info()
        print("info",self)
    end
    
    local a = A()
    local b = A()
    
    a:info()
    b:info()
    
    ---- result output ----
    info    table: 0x7f8771e05030
    info    table: 0x7f8771e050a0
    

    __metatable

    原文翻译:

    隐藏真正的元表,当调用getmetatable时,而且table的元表有__metatable字段,则返回__metatable字段中的值。

    测试代码:

    local meta = {
        name = "meta"
    }
    
    local test = setmetatable({}, meta)
    print(getmetatable(test).name)
    
    local meta = {
        __metatable = {name = "__metatable"},
        name = "meta"
    }
    
    local test = setmetatable({}, meta)
    print(getmetatable(test).name)
    
    ---- result output ----
    meta
    __metatable
    

    结果很直观不解释了,我另外还做了个的测试,让__metatable指向了一个function,调用getmetatable时也会返回这个function。很有意思,但是暂时没想到有什么应用场景。

    __tostring

    原文翻译:

    控制字符串的表现,当调用tostring(myTable)时,且myTable的元表中有__tostring字段时,就会调用这个方法。返回值是方法的返回值。

    测试代码:

    local meta = {
        __tostring = function(t)
            return string.format("My name is %s", t.name)
        end
    }
    
    local test = setmetatable({}, meta)
    test.name = "test"
    print(test)
    print(tostring(test))
    
    ---- result output ----
    My name is test
    My name is test
    

    这个也不做过多说明了,很容易理解。有一点提一下就是print方法会自动调用tostring(test)

    __len

    原文翻译:

    控制table的长度。当用#操作符请求长度时,且table的元表有__len字段指向一个function,就会调用这个function,参数是table自己,返回值是function的返回值。

    写个测试代码:

    local meta = {
        __len = function(t)
            local result = 0
            for k, v in pairs(t) do
                result = result + 1
            end
            return result
        end
    }
    
    
    local test = {
        [1] = "A",
        [2] = "B",
        [3] = "C",
        [5] = "D",
        [6] = "E",
        [8] = "F",
    }
    print(#test)
    
    setmetatable(test, meta)
    print(#test)
    
    ---- result output ----
    3
    6
    

    Lua中使用#获取长度有个特性,就是如果某个key对应的值是nil就结束,上面例子中,test第4个值是nil,那返回的长度为3。我们重新定义了__len后返回了,用遍历的方式计算长度,返回table内元素的数量为6。

    __gc

    原文翻译:

    简单说就是数据被垃圾回收的时候会首先触发__gc

    测试代码:

    local meta = {
    
        __gc = function(t)
            print("gc")
        end
    }
    
    local function test()
        local test = {}
        setmetatable(test, meta)
    end
    
    test()
    
    ---- result output ----
    gc
    

    Lua table中所有的元方法就分析完了,还有一些操作符重载的,不细说了。

    相关文章

      网友评论

          本文标题:深度理解Lua中的table元方法

          本文链接:https://www.haomeiwen.com/subject/nooydqtx.html