【读书笔记】语言特性-元表和元方法(三)

目录

一. 元表和元方法的介绍

        1. 元表是一类特殊的表,能修改一个值在面对一个未知操作时的行为。

        2. Lua语言中的每一个值都可以有元表。其中,每一个表和用户数据类型都具有各自独立的元表,而其他类型的值共享对应类型所属的同一元表

        3. 在Lua语言中,只能为表设置元表;如果要给其他类型的值设置元表,则必须通过C代码或调试库完成。

        4. 字符串标准库为所有的字符串都设置了同一个元表,而其他类型默认情况下没有元表 (字符串默认有元表,其他类型没有)

二 算术运算相关的元方法

        2.1 Demo: 一个用于集合的简单模块

        第二步: 假设想使用加法操作符来计算两个集合的并集,那么可以让所有集合的表共享一个元表,在通过定义元方法__add 实现

        第三步: 在元表中加入元方法 __add

        类似地,也可以使用乘法运算符计算集合的交集

        2.2 算术运算符的元方法

3. 关系运算相关的元方法

3.1 其他三个关系运算符 不等于、大于、大于等于的实现

4. 库相关的元方法

5. 表相关的元方法

        5.1 __index元方法:用于表的查询,可以应用于继承、创建具有默认值的表等

                5.1.1 原理:当访问一个表中不存在的字段时会得到 nil ,实际上还会 引发解释器查找一个名为 __index的元方法,如果没有这个元方法,结果就返回 nil, 如果有则使用元方法提供的值

                5.1.2 实现方式:在上例中,Lua语言发现w中没有对应的字段“width”,但却有一个带有 __index元方法的元表,Lua语言会以 w(表)和 "width" (不存在的键)为参数调用这个元方法,元方法随后会用这个键检索原型返回结果

                5.1.4 当希望访问一个表时不调用__index 元方法,可以使用函数 rawget. 调用rawget(t, i) 会对表t 进行原始访问(不考虑元表,简单对表进行访问)

        5.2 __newindex元方法:用于表的更新,可用作 实现只读的表

                     5.2.1 原理:当对一个表中不存在的索引赋值时,解释器会查找__newindex元方法, 如果元方法存在,那么解释器就调用它而不进行赋值

                        5.2.2 调用rawset(t, k, v) 等价于 t[k] = v ,绕过元方法

        5.3 具有默认值的表

        5.4 跟踪对表的访问

        5.5 只读的表


一. 元表和元方法的介绍

        通常,Lua语言中的每种类型的值都有一套操作集合。例如,我们可以把数字相加,可以连接字符串等。但是,我们无法将两个表相加,无法对函数作比较,除非使用元表

        1. 元表是一类特殊的表,能修改一个值在面对一个未知操作时的行为

                例如,表a和表b 试图相加时,Lua会检查两表之一是否存在元表且 该元表中是否有_add字段,存在该字段,就调用该字段对应的值(即 所谓的元方法 metamethod

        2. Lua语言中的每一个值都可以有元表。其中,每一个表和用户数据类型都具有各自独立的元表,而其他类型的值共享对应类型所属的同一元表

        

t = {}
print(getmetatable(t)) -- > nil
t1 = {}
setmetatable(t, t1) -- 将t1设置成t的元表
print(getmetatable(t) == t1) --> true

        3. 在Lua语言中,只能为表设置元表;如果要给其他类型的值设置元表,则必须通过C代码或调试库完成。

        4. 字符串标准库为所有的字符串都设置了同一个元表,而其他类型默认情况下没有元表 (字符串默认有元表,其他类型没有)

print(getmetatable("hi"))  -- table:0x807772e0
print(getmetatable("xuxu"))  -- table:0x807772e0
print(getmetatable(10))	-- nil
print(getmetatable(print)) -- nil

二 算术运算相关的元方法

        2.1 Demo: 一个用于集合的简单模块

local Set = {}

-- 使用指定的列表创建一个新的集合
function Set.new(l)
    local set = {}
    for _, v in ipairs(l) do set[v] = true end
    return set
end

-- 并集
function Set.union(a, b)
    local res = Set.new{}
    for k in pairs(a) do res[k] = true end
    for k in pairs(b) do res[k] = true end
    return res
end

-- 交集
function Set.intersection(a, b)
    local res = Set.new{}
    for k in pairs(a) do
    	res[k] = b[k] -- 如 b[k]不存在,则b[k]为nil, res[k] = nil 意味着删除 该元素,所以 res[k]只会保存 true
    end
    return res
end

function Set.tostring(set)
    local l = {}
    for e in pairs(set) do
    	l[#l + 1] = tostring(e)
    end
    return "{" .. table.concat(l, ", ") .. "}"
end

return Set

        第二步: 假设想使用加法操作符来计算两个集合的并集,那么可以让所有集合的表共享一个元表,在通过定义元方法__add 实现

local mt = {} -- 集合的元表
function Set.new(l)
    local set = {}
    setmetatable(set, mt)
    for _, v in ipairs(l) do set[v] = true end
    return set
end

        这样,所有由Set.new创建的集合都具有一个相同的元表了

s1 = Set.new{10,20,30,50}
s2 = Set.new{30, 1}
print(getmetatable(s1))	-- table:0x00672B60
print(getmetatable(s2))	-- table:0x00672B60

        第三步: 在元表中加入元方法 __add

mt.__add = Set.union
s3 = s1 + s2
print(Set.tostring(s3))	--> {1, 10, 20, 30, 50}

        类似地,也可以使用乘法运算符计算集合的交集

mt._mul = Set.intersection
print(Set.tostring(s1 + s2)*s1)	--> {10, 20, 30, 50}

        2.2 算术运算符的元方法

        

元方法

含义

__add

加法

__mul

乘法

__sub

减法

__div

除法

__idiv

floor除法

__unm

负数

__mod

取模

__pow

幂运算

__band

按位与

__bor

按位或

__bxor

按位异或

__bnot

按位取反

__shl

向左移位

__shr

向右移位

3. 关系运算相关的元方法

        

元方法

含义

__eq

等于

__lt

小于

__le

小于等于

3.1 其他三个关系运算符 不等于大于大于等于的实现

        由于这三个没有单独的元方法,可通过上面的转换得出

        不等于: a ~= b 装换为 not(a == b)

        大于: a > b 装换为 b < a

        大于等于:a >= b 装换为 b

-- 实现小于等于,子集关系
mt.__le = function (a, b)
    for k in pairs(a) do
    	if not b[k] then return false end
    end
    return true
end

-- 实现小于,真子集
mt.__lt = function(a, b)
    return a <= b and not (b <= a)
end
-- 实现等于,判断两集合是否相等
mt.__eq = function (a, b)
    return a <= b and b <= a
end
s1 = Set.new{2, 4}
s2 = Set.new{4, 10, 2}
print(s1 <= s2)	-- true
print(s1 < s2)	-- true
print(s1 >= s1)	-- true
print(s1 > s1)	-- false
print(s1 == s2 * s1) -- true

4. 库相关的元方法

         程序库在元表中定义和使用它们自己的字段也是一种常用的实践

        例如:

print({})	-- table:0x8062ac0
-- 输出一个字符串

        函数print 总是调用 函数tostring 来进行格式化输出,不过,当对值进行格式化时,函数tostring 会首先检查值是否有一个元方法 __tostring, 如果有,函数tostring 就调用这个元方法,将对象作为参数传给 该函数,然后把元方法的返回值作为函数tostring 的返回值

        在之前集合的示例中,已经定义了一个将集合表示为字符串的函数,只需要在元表中设置 __tostring 字段:

mt.__tostring = Set.tostring
s1 = Set.new{10, 4, 5}
print(s1)	-- {4, 5, 10}

5. 表相关的元方法

        5.1 __index元方法:用于表的查询,可以应用于继承、创建具有默认值的表等

                Demo: 用继承的概念,创建一个窗口对象

prototype = {x = 0, y = 0, width = 100, height = 100} -- 创建默认值原型表
local mt = {} --创建一个元表

-- 声明构造函数
function new(o)
    setmetatable(o, mt)
    return o
end
--定义元方法
mt.__index = function (_, key)
    return prototype[key]
end
w = new{x=10,  y=20}
print(w.width)	-- 100

                5.1.1 原理:当访问一个表中不存在的字段时会得到 nil ,实际上还会 引发解释器查找一个名为 __index的元方法,如果没有这个元方法,结果就返回 nil, 如果有则使用元方法提供的值

                5.1.2 实现方式:在上例中,Lua语言发现w中没有对应的字段“width”,但却有一个带有 __index元方法的元表,Lua语言会以 w(表)"width" (不存在的键)为参数调用这个元方法,元方法随后会用这个键检索原型返回结果

                5.1.3 元方法 __index 不一定必须是函数,也可以是一个表,当元方法是函数时,Lua语言会以表和不存在的键为参数调用该函数;当元方法是一个表时,Lua语言就访问这个表

mt.__index = prototype

                5.1.4 当希望访问一个表时不调用__index 元方法,可以使用函数 rawget. 调用rawget(t, i) 会对表t 进行原始访问(不考虑元表,简单对表进行访问)

        5.2 __newindex元方法:用于表的更新,可用作 实现只读的表

                     5.2.1 原理:当对一个表中不存在的索引赋值时,解释器会查找__newindex元方法, 如果元方法存在,那么解释器就调用它而不进行赋值

                        5.2.2 调用rawset(t, k, v) 等价于 t[k] = v ,绕过元方法

        5.3 具有默认值的表

                一个普通表中所有字段的默认值都是 nil。通过元表,可以很容易修改这个默认值:

function setDefault(t, d)
    local mt = {__index = function() return d end}
    setmetatable(t, mt)
end
tab = {x = 10, y = 20}
print(tab.x, tab.z)
setDefault(tab, 0)
print(tab.x, tab.z) -- 10, 0

                函数setDefault 为所有需要默认值的表创建了一个新的闭包和一个新的元表,如果此时需要很多默认值的表,那开销就会很大,可以把 创建元表的方法放到函数外部,只创建一遍

local mt = {__index = function(t) return t.__ end}
function setDefault()
    t.___ = d
    setmetatable(t, mt)
end

        这样,每个表中的 ___字段 都会作为默认值返回

        如果 担心命名冲突那需要把___字段替换成一个唯一的值:可以用空表作为 表的键,这样,key值就是唯一的        

local key = {}
local mt = {__index = function (t) return t[key] end}
function setDefault()
    t[key] = d
    setmetatable(t, mt)
end

        

        5.4 跟踪对表的访问

function track(t)
    local proxy = {} -- 为代理创建元表
    
    local mt = {
        __index = function (_, k)
        	print("*access to element " .. tostring(k))
         	return t[k] -- 访问原来的表
        end,
        
        __newindex = function(_, k, v)
        	print("*update of element " .. tostring(k) .. " to" .. tostring(v))
         t[k] = v
        end,
        
        __pairs = function ()
        	return function (_, k)
         	local nextkey, nextvalue = next(t, k)
          	if nextkey ~= nil then
           print("traversing element " .. tostring(nextkey))
           end
           return nextkey, nextvalue
         	end
        end,
        
        __len = function () return #t end
    }
    setmetatable(proxy, mt)
    return proxy
end
t = {}
t = track(t)
t[2] = "hello"
    -- *update of element 2 to hello
print(t[2])
    -- *access to element 2
    -- hello

        元方法 __ index 和__newindex 按照我们设计的规则跟踪每一个访问并将其重定向到原来的表中 。元方法__pairs 使得我们能够像遍历原来的表一样遍历代理,从而跟踪所有的访问。

        最后,元方法__ len 通过代理实现了长度操作符:

t = track({10, 20})
print(#t)	--2
for k, v in pairs(t) do print(k, v) end
    -- *traversing element 1
    -- 1	10
    -- traversing element 2
    -- 2	20

        5.5 只读的表

                可以使用代理(空表做代理)的概念来实现只读的表,需要跟踪表的更新操作并抛出异常即可:

function readOnly(t)
    local proxy = {}
    local mt = {
        __index = t,
        __newindex = function (t, k, v)
    	error("attempt to update a read-only table", 2)
        end
    }
    setmetatable(proxy, mt)
    return proxy
end
days = readOnly{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
print(days[1])  --Sunday
days[2] = "Noday"
-- stdin:1: attempt to update a read-only table

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值