Lua是一门动态类型的语言,这意味着Lua中的变量没有类型,而值才有类型。一个变量可以在不同时刻指向不同类型的值。下面将对Lua中的值及其类型做一些总结。
基本数据类型及其子类型
在lua-5.3.5版本中,有9中基本的数据类型,其定义如下:
1 | #define LUA_TNIL 0 // 空类型 |
其中需要说明的是LUA_TLIGHTUSERDATA和LUA_TUSERDATA的区别是:前者的所有对象共享一个元表,且其内部所指向的内存的申请及释放不需要由Lua来完成;后者的每个对象都有自己的元表,需要进行垃圾回收,并且其内部所指向的内存的申请和释放需要由Lua来完成。除此之外,上述的一些基本类型有子类型(变种类型),一些基本类型需要GC,例如LUA_TNUMBER类型可以进一步分为整数类型和实数类型,现将这些子类型及GC标记的定义一并罗列如下:
1 | #define LUA_TLCL (LUA_TFUNCTION | (0 << 4)) /* Lua closure */ |
Lua中是如何实现对基本类型、子类型、是否需要GC等的表示的呢?其实从子类型的定义我们也可以大概了解到,具体如下:
- 基本类型有9种,因此可以用低四位,即bits 0-3来表示;
- 每种基本类型的子类型不超过4种,因此可以用中间两位,即bits 4-5来表示;
- 用于标记某种类型是否需要进行GC,只需要一个位,因此可以用bit 6来表示;
值表示的统一数据结构
Lua中所有的值都是第一类值,即所有的值都可以存储在变量中,也可以作为参数传递给函数,也可以作为函数的返回值。为了更好地管理Lua中的值,Lua使用了一个通用的数据结构TValue来存储Lua中可能出现的任何值及其类型,其定义如下:
1 | typedef union Value { |
联合体Value将Lua中所有可能的值都囊括了进来,比如通过包含GCObject *类型的成员gc,可以将所有需要GC的值都包含进来。结构体TValue包含了两个成员,一个是联合体类型Value的对象value_,用于表示Lua中所有可能出现的值,另一个是int类型的tt_,用来表示Value具体表示哪种类型的值。这样通过TValue就是可以表示Lua中所有的值及其类型了。
那如何将TValue与某种具体类型的值之间做转换呢?其主要的逻辑是将TValue中的value_及tt_与具体的数据及其类型对应起来做转换。以TValue和LUA_TNUMFLT之间的转换为例:
1 | /* Macro to test type */ |
我们可以通过宏fltvalue()从TValue对象中取出其中存放的实数,通过宏setfltvalue()将实数存放到TValue对象中,并将其内部类型设置为LUA_TNUMFLT。
垃圾回收类型及其“继承”
上面说到,Value将Lua中所有可能的值都包含了进来,从它的定义中我们不难看出,它确实将布尔类型、整数类型、实数类型、轻量级函数类型等类型的值包含了进来,只要给Value类型的对象赋其中某一个类型的值就行,但是为什么说通过包含GCObject *类型的gc成员就将所有需要GC的类型的值也包含进来了呢?我们先来看下GCObject的定义:
1 | /* Common type for all collectable objects */ |
为了更好地说明上面提出来的问题,我们通过一个需要进行GC的类型TString(即lua中的字符串类型)来进行辅助说明。从上面GCObject类型和TString类型的定义中可以看出,两者均在开头包含了一个宏CommonHeader。这就使得这两个类型所对应的对象的内存布局的头部是相同的,两者的内存布局如图1所示,而TString类型的对象中多出来的内容则是其私有数据,因而所有需要GCObject类型的对象地方就都可以将TString类型的对象传进去,当然需要做一个强制类型转换(毕竟是伪继承),将TString类型强转伪GCObject类型,在实际使用的时候,我们再将其强转回TString类型即可。所有其他需要进行GC的类型都和TString一样,会在定义的开头加上CommonHeader,从而实现类似的功能。从另外一方面看,我们可以将GCObject看成TString、Table等的父类,这也很好地体现了在C中如何实现继承等面向对象编程的方法。
1 | /* 创建一个新的字符串对象,未填入具体的字符串内容,只是申请了内存空间 */ |
注意到上面的例程中有一条将GCObject类型对象强转为TString类型对象的语句。为了方便做类似的转换,Lua专门定义了一个联合体及一系列的宏来辅助进行这样的操作:
1 | union GCUnion { |
为了节省篇幅,我们还是以GCObject类型强转TString为例。在createstrobj()这个创建TString类型的对象的例程中,我们先计算了我们要创建的TString对象的大小,包括TString结构体本身(字符串管理结构)以及实际内容的总大小,然后以字符串类型(可以是长字符串,也可以是短字符串)及对象大小来调用luaC_newobj()函数创建一个GCObject类型的对象(在当前上下文中,这是一个伪装成GCObject的TString),然后调用gco2ts()这个宏将其转换为TString类型的对象。这个宏的关键点是“&((cast_u(o))->ts)”,由于GCUnion是一个联合体,而该联合体内囊括的所有类型又都是需要GC的类型,那么可以说联合体GCUnion的一个对象和所有需要进行GC的类型的对象的内存布局的开头是一样的,正如GCObject和TString的内存布局开头那样,那么关键点中的“cast_u(o)”是可行的,而且不会引起错误,然后我们再以TString的方式来访问这个联合体,即关键点的剩余部分“&((cast_u(o))->ts)”,这样的效果就是将GCObject对象转换为了TString对象。其他需要GC的类型也可以用类似方式进行转换。
本文关于Lua中值及其类型的总结就到这里了。
参考:
1、 Lua 5.3 Reference Manual
2、 《Lua设计与实现》