Lua是一门动态类型的语言,这意味着Lua中的变量没有类型,而值才有类型。一个变量可以在不同时刻指向不同类型的值。下面将对Lua中的值及其类型做一些总结。
基本数据类型及其子类型 在lua-5.3.5版本中,有9中基本的数据类型,其定义如下:
1 2 3 4 5 6 7 8 9 10 11 #define LUA_TNIL 0 // 空类型 #define LUA_TBOOLEAN 1 // 布尔类型 #define LUA_TLIGHTUSERDATA 2 // 指针类型(void *) #define LUA_TNUMBER 3 // 数字类型 #define LUA_TSTRING 4 // 字符串类型 #define LUA_TTABLE 5 // 表类型 #define LUA_TFUNCTION 6 // 函数类型 #define LUA_TUSERDATA 7 // 指针类型(void *) #define LUA_TTHREAD 8 // Lua虚拟机、协程 #define LUA_NUMTAGS 9
其中需要说明的是LUA_TLIGHTUSERDATA和LUA_TUSERDATA的区别是:前者的所有对象共享一个元表,且其内部所指向的内存的申请及释放不需要由Lua来完成;后者的每个对象都有自己的元表,需要进行垃圾回收,并且其内部所指向的内存的申请和释放需要由Lua来完成。除此之外,上述的一些基本类型有子类型(变种类型),一些基本类型需要GC,例如LUA_TNUMBER类型可以进一步分为整数类型和实数类型,现将这些子类型及GC标记的定义一并罗列如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #define LUA_TLCL (LUA_TFUNCTION | (0 << 4)) /* Lua closure */ #define LUA_TLCF (LUA_TFUNCTION | (1 << 4)) /* light C function */ #define LUA_TCCL (LUA_TFUNCTION | (2 << 4)) /* C closure */ #define LUA_TSHRSTR (LUA_TSTRING | (0 << 4)) /* short strings */ #define LUA_TLNGSTR (LUA_TSTRING | (1 << 4)) /* long strings */ #define LUA_TNUMFLT (LUA_TNUMBER | (0 << 4)) /* float numbers */ #define LUA_TNUMINT (LUA_TNUMBER | (1 << 4)) /* integer numbers * /* Bit mark for collectable types */ #define BIT_ISCOLLECTABLE (1 << 6) /* mark a tag as collectable */ #define ctb(t) ((t) | BIT_ISCOLLECTABLE)
Lua中是如何实现对基本类型、子类型、是否需要GC等的表示的呢?其实从子类型的定义我们也可以大概了解到,具体如下:
基本类型有9种,因此可以用低四位,即bits 0-3来表示;
每种基本类型的子类型不超过4种,因此可以用中间两位,即bits 4-5来表示;
用于标记某种类型是否需要进行GC,只需要一个位,因此可以用bit 6来表示;
值表示的统一数据结构 Lua中所有的值都是第一类值,即所有的值都可以存储在变量中,也可以作为参数传递给函数,也可以作为函数的返回值。为了更好地管理Lua中的值,Lua使用了一个通用的数据结构TValue来存储Lua中可能出现的任何值及其类型,其定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef union Value { GCObject *gc; /* collectable objects */ void *p; /* light userdata */ int b; /* booleans */ lua_CFunction f; /* light C functions */ lua_Integer i; /* integer numbers */ lua_Number n; /* float numbers */ } Value; #define TValuefields Value value_; int tt_ typedef struct lua_TValue { TValuefields; } TValue;
联合体Value将Lua中所有可能的值都囊括了进来,比如通过包含GCObject *类型的成员gc,可以将所有需要GC的值都包含进来。结构体TValue包含了两个成员,一个是联合体类型Value的对象value_,用于表示Lua中所有可能出现的值,另一个是int类型的tt_,用来表示Value具体表示哪种类型的值。这样通过TValue就是可以表示Lua中所有的值及其类型了。 那如何将TValue与某种具体类型的值之间做转换呢?其主要的逻辑是将TValue中的value_及tt_与具体的数据及其类型对应起来做转换。以TValue和LUA_TNUMFLT之间的转换为例:
1 2 3 4 5 6 7 8 9 10 11 12 /* Macro to test type */ #define ttisfloat(o) checktag((o), LUA_TNUMFLT) /* Macro to access values */ #define fltvalue(o) check_exp(ttisfloat(o), val_(o).n) #define val_(o) ((o)->value_) #define settt_(o,t) ((o)->tt_=(t)) /* Macro to set value */ #define setfltvalue(obj,x) { TValue *io=(obj); val_(io).n=(x); settt_(io, LUA_TNUMFLT); }
我们可以通过宏fltvalue()从TValue对象中取出其中存放的实数,通过宏setfltvalue()将实数存放到TValue对象中,并将其内部类型设置为LUA_TNUMFLT。
垃圾回收类型及其“继承” 上面说到,Value将Lua中所有可能的值都包含了进来,从它的定义中我们不难看出,它确实将布尔类型、整数类型、实数类型、轻量级函数类型等类型的值包含了进来,只要给Value类型的对象赋其中某一个类型的值就行,但是为什么说通过包含GCObject *类型的gc成员就将所有需要GC的类型的值也包含进来了呢?我们先来看下GCObject的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /* Common type for all collectable objects */ typedef struct GCObject GCObject; /* Common Header for all collectable objects (in macro form, to be included in other objects) */ #define CommonHeader GCObject *next; lu_byte tt; lu_byte marked /* Common type has only the common header */ struct GCObject { CommonHeader; }; typedef struct TString { CommonHeader; lu_byte extra; /* reserved words for short strings; "has hash" for longs */ lu_byte shrlen; /* length for short strings */ unsigned int hash; union { size_t lnglen; /* length for long strings */ struct TString *hnext; /* linked list for hash table */ } u; } TString;
为了更好地说明上面提出来的问题,我们通过一个需要进行GC的类型TString(即lua中的字符串类型)来进行辅助说明。从上面GCObject类型和TString类型的定义中可以看出,两者均在开头包含了一个宏CommonHeader。这就使得这两个类型所对应的对象的内存布局的头部是相同的,两者的内存布局如图1所示,而TString类型的对象中多出来的内容则是其私有数据,因而所有需要GCObject类型的对象地方就都可以将TString类型的对象传进去,当然需要做一个强制类型转换(毕竟是伪继承),将TString类型强转伪GCObject类型,在实际使用的时候,我们再将其强转回TString类型即可。所有其他需要进行GC的类型都和TString一样,会在定义的开头加上CommonHeader,从而实现类似的功能。从另外一方面看,我们可以将GCObject看成TString、Table等的父类,这也很好地体现了在C中如何实现继承等面向对象编程的方法。
从上面的分析我们可以知道,GCObject类型可以说是所有需要进行垃圾回收的类型的代表。在一些接口方面,都是以GCObject类型进行呈现的。例如在Lua中,进程要创建一个需要进行垃圾回收类型的对象时,都是申请一个GCObject对象,同时将具体的类型和所需要的内存大小通过参数传递进去。申请GCObject对象成功后,在函数外层再根据上下文环境将GCObject对象转换为具体类型的对象,再进行类型私有数据的初始化等操作。还是以TString为例,假设要创建一个TString类型的对象,lua源码中有如下例程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 /* 创建一个新的字符串对象,未填入具体的字符串内容,只是申请了内存空间 */ static TString *createstrobj (lua_State *L, size_t l, int tag, unsigned int h) { TString *ts; GCObject *o; size_t totalsize; /* total size of TString object */ /* 计算字符串对应需要的内存大小,包括头部和内容,内容紧跟在头部之后 */ totalsize = sizelstring(l); /* 根据存放字符串所需的内存大小和类型标记创建一个新的GCObject对象 */ o = luaC_newobj(L, tag, totalsize); /* 将GCObject类型转换为具体的TString类型 */ ts = gco2ts(o); /* 保存字符串对应的hash值 */ ts->hash = h; ts->extra = 0; /* 字符串以'版权声明: 本博客所有文章除特别声明外,均采用 null 许可协议。转载请注明来自 安全书 !