Skip to content

Latest commit

 

History

History
215 lines (171 loc) · 8.33 KB

ch05-元表.md

File metadata and controls

215 lines (171 loc) · 8.33 KB

##元表及面向对象的实现 ###元表的实现原理 Lua在初始化时,首先会调用函数luaT_init初始化Lua中定义的几种元方法对应的字符串,这些都是全局共用的,在初始化完毕之后只可读不可写,也不能回收:

(ltm.c)
30 void luaT_init (lua_State *L) {
31   static const char *const luaT_eventname[] = {  /* ORDER TM */
32     "__index", "__newindex",
33     "__gc", "__mode", "__eq",
34     "__add", "__sub", "__mul", "__div", "__mod",
35     "__pow", "__unm", "__len", "__lt", "__le",
36     "__concat", "__call"
37   };
38   int i;
39   for (i=0; i<TM_N; i++) {
40     G(L)->tmname[i] = luaS_new(L, luaT_eventname[i]);
41     luaS_fix(G(L)->tmname[i]);  /* never collect these names */
42   }
43 }

在这里,将遍历前面定义的枚举类型TMS,将每一个类型对应的字符串赋值给global_State结构体中的tmname,同时调用函数luaS_fix,将这些字符串设置为不可回收的,因为在这个系统运行的过程中,这些字符串会一直被使用到,至于如何让它们变成不可回收的,后面GC的部分将做分析.

(ltm.c)
61 const TValue *luaT_gettmbyobj (lua_State *L, const TValue *o, TMS event) {
62   Table *mt;
63   switch (ttype(o)) {
64     case LUA_TTABLE:
65       mt = hvalue(o)->metatable;
66       break;
67     case LUA_TUSERDATA:
68       mt = uvalue(o)->metatable;
69       break;
70     default:
71       mt = G(L)->mt[ttype(o)];
72   }
73   return (mt ? luaH_getstr(mt, G(L)->tmname[event]) : luaO_nilobject);
74 }

根据不同的数据类型,返回元方法的函数,只有在数据类型为Table以及udata的时候,才能拿到对象的metatable表,其他时候是到global_State结构体的成员mt来获取的,但是这个对于其他的数据类型而言,一直是空值.

(ltm.h)
41 #define gfasttm(g,et,e) ((et) == NULL ? NULL : \
42   ((et)->flags & (1u<<(e))) ? NULL : luaT_gettm(et, e, (g)->tmname[e]))
43
44 #define fasttm(l,et,e)  gfasttm(G(l), et, e)

(ltm.c)
50 const TValue *luaT_gettm (Table *events, TMS event, TString *ename) {
51   const TValue *tm = luaH_getstr(events, ename);
52   lua_assert(event <= TM_EQ);
53   if (ttisnil(tm)) {  /* no tag method? */
54     events->flags |= cast_byte(1u<<event);  /* cache this fact */
55     return NULL;
56   }
57   else return tm;
58 }

这里,真正做查找工作的,是函数luaT_gettm,但是外面直接调用的,却是宏fasttm和宏gfasttm,两者的区别在于一个使用的参数是lua_State指针,一个使用的参数是global_State指针.这里会做一个优化,当第一次查找表中的某个元方法并且没有找到时,会将Table中的flags成员对应的位做置位操作,这样下一次再来查找该表中同样的元方法时如果该位已经为1,那么直接返回NULL即可.

(lvm.c)
108 void luaV_gettable (lua_State *L, const TValue *t, TValue *key, StkId val) {
109   int loop;
110   for (loop = 0; loop < MAXTAGLOOP; loop++) {
111     const TValue *tm;
112     if (ttistable(t)) {  /* `t' is a table? */
113       Table *h = hvalue(t);
114       const TValue *res = luaH_get(h, key); /* do a primitive get */
115       if (!ttisnil(res) ||  /* result is no nil? */
116           (tm = fasttm(L, h->metatable, TM_INDEX)) == NULL) { /* or no TM? */
117         setobj2s(L, val, res);
118         return;
119       }
120       /* else will try the tag method */
121     }
122     else if (ttisnil(tm = luaT_gettmbyobj(L, t, TM_INDEX)))
123       luaG_typeerror(L, t, "index");
124     if (ttisfunction(tm)) {
125       callTMres(L, val, tm, t, key);
126       return;
127     }
128     t = tm;  /* else repeat with `tm' */
129   }
130   luaG_runerror(L, "loop in gettable");
131 }

这个函数的逻辑,是一个逐层根据该对象的metable表中的"__index"来查找的过程:

1.如果t是一个Table,尝试根据key在该表中查找数据,在找到了非空数据,或者该表的metatable中"__index"为空的情况下,都返回查找的结果.此时,在不返回的情况只有可能是在原表中查找的数据为空同时原表的metatable存在"__index"成员,而且此时该成员已经赋值给了tm.
2.来到这种情况,说明前面判断t不是一个Table,于是调用luaT_gettmbyobj函数,尝试拿到这个数据的metatable["__index"],如果返回空,那么报错返回.
3.此时,tm不是一个空值,于是判断前面拿到的tm是不是一个函数,如果是,那么通过callTMres函数来调用它,调用之后返回.
4.来到这里,说明前面得到的tm,既不是一个空值,也不是一个函数,而是前面第一种情况得到的t->metatable["__index"],将这个值赋值做为下一个循环中处理的t,继续前面的操作.
5.如果这个前面的这个循环处理,层次过多,超过了MAXTAGLOOP,那么就终止循环报错返回.

有了luaV_gettable的操作,对应的就会有luaV_settable,原理与luaV_gettable差不多,区别在于:

1. luaV_settable是查找元表中的"__newindex"成员,同时,由于set操作导致的表的成员发生了增加,需要调用luaC_barriert对该Table做屏障操作,具体的原理在后面GC部分将做说明.
2. 当元表中的"__newindex"成员是函数的情况下,调用的是callTM函数.

###在Lua中实现面向对象 有了前面的基础,理解如何在Lua中实现面向对象就不难了,这里已经没有需要分析的Lua解释器代码,仅需要看一个具体的例子:

(base.lua)
module( "base", package.seeall )
function new( )
	local obj = {}
	setmetatable( obj, { __index = base } )
	return obj
end

(test.lua)
module( "test", package.seeall )
setmetatable( test, {__index = base} )

function new( )
	local obj = {}
	setmetatable( obj, { __index = test } )
	return obj
end

在这个例子里,提供了两个Lua模块,其中base模块可以认为是基类模块,而test是继承自base的子类. 在base.lua中,使用"package.seeall"的模块定义,让该模块中的全部方法对外暴露;而在test中,在最开始调用setmetatable将base模块的Table直接赋值做为test模块的metatable.后面每次调用test的new函数创建对象时,都在返回之前设置该对象的metatable,这样根据前面的分析,每次查找该对象的某个成员,如果在test模块中没有定义,会往上根据metatable的"__index"成员到base模块中查找.

这就是Lua中实现面向对象的基本原理及演示.

但是,由前面的分析可知,查找基类的成员,需要一层一层的往上查找,这个过程还是有性能上的损耗的.所以另一种实现面向对象的方式,是直接将基类的成员深拷贝给子类,在云风的博客中就是使用类似的方案:

local _class={}
 
function class(super)
	local class_type={}
	class_type.ctor=false
	class_type.super=super
	class_type.new=function(...) 
			local obj={}
			do
				local create
				create = function(c,...)
					if c.super then
						create(c.super,...)
					end
					if c.ctor then
						c.ctor(obj,...)
					end
				end
 
				create(class_type,...)
			end
			setmetatable(obj,{ __index=_class[class_type] })
			return obj
		end
	local vtbl={}
	_class[class_type]=vtbl
 
	setmetatable(class_type,{__newindex=
		function(t,k,v)
			vtbl[k]=v
		end
	})
 
	if super then
		setmetatable(vtbl,{__index=
			function(t,k)
				local ret=_class[super][k]
				vtbl[k]=ret
				return ret
			end
		})
	end
 
	return class_type
end

来看看怎么使用,首先是基类的定义:

base_type=class()		-- 定义一个基类 base_type
 
function base_type:ctor(x)	-- 定义 base_type 的构造函数
	print("base_type ctor")
	self.x=x
end
 
function base_type:print_x()	-- 定义一个成员函数 base_type:print_x
	print(self.x)
end
 
function base_type:hello()	-- 定义另一个成员函数 base_type:hello
	print("hello base_type")
end

其次根据该基类定义一个继承自base_type的子类:

test=class(base_type)	-- 定义一个类 test 继承于 base_type
 
function test:ctor()	-- 定义 test 的构造函数
	print("test ctor")
end
 
function test:hello()	-- 重载 base_type:hello 为 test:hello
	print("hello test")
end

使用方式:

a=test.new(1)	-- 输出两行,base_type ctor 和 test ctor 。这个对象被正确的构造了。
a:print_x()	-- 输出 1 ,这个是基类 base_type 中的成员函数。
a:hello()	-- 输出 hello test ,这个函数被重载了。 

出自链接: http://blog.codingnow.com/cloud/LuaOO