深度理解Lua中的table元方法

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中所有的元方法就分析完了,还有一些操作符重载的,不细说了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,186评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,858评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,620评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,888评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,009评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,149评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,204评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,956评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,385评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,698评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,863评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,544评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,185评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,899评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,141评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,684评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,750评论 2 351

推荐阅读更多精彩内容