在上一篇《理解 Ruby Symbol ,第 1 部分:使用 Symbol 》中,我们大致了解了 Symbol,包括 Symbol 和 String 的区别、 Symbol 的使用等。本文我们将深入到 Ruby 内部去看看 Symbol 的样子,这对更好理解和使用 Symbol 可能会有些帮助。
Ruby 是用 C 语言实现的,本文我们将以 Ruby 1.8.6 版本实现为例。
Ruby 对象
Ruby 是一个完全面向对象的语言,这点很好地体现在对象方法调用的一致性上,一个对象,无论是如何创建的,调用实例方法的方式是一样的,即“<对象>.<方法名>”。
Ruby对象的方法调用
"gin joint".length 9 "Rick".index("c") 2 -1942.abs 1942
自然 Symbol 也是一种对象,那么 Ruby 内部是如何一致地表示各种对象的呢?
对象定义
VALUE(ruby.h)
typedef unsigned long VALUE;
没错,这就是 Ruby 对象的定义,确切地说是对象的引用。在 Ruby 中绝大多数对象的内容表示为 C 语言中的结构体,比如用户定义的对象和 Ruby 预定义的对象如 String, Array 等。我们知道引用结构体的方法是指针, void *
指针是比较通用的,使用之前先转换为待引用结构体类型的指针。那么为什么不使用 void *
作为对象的引用呢?
这是因为Ruby在 VALUE 中内嵌了其他类型的,不用结构体表示的对象,也就是说直接用VALUE 表示的对象,这其中就有 Symbol 。考虑到现在大多数计算机体系上 void *
指针和 sizeof(unsigned long)
大小是相同的(比如4字节),可以互相转换,因此使用VALUE 更有价值。
内嵌到 VALUE 中的对象有 Fixnum,Symbol,true,false,nil 和 undef 。
Fixnum(ruby.h)
#define FIXNUM_MAX (LONG_MAX>>1) #define FIXNUM_MIN RSHIFT((long)LONG_MIN,1) #define FIXNUM_FLAG 0x01 #define INT2FIX(i) ((VALUE)(((long)(i))<<1 | FIXNUM_FLAG)) #define LONG2FIX(i) INT2FIX(i) #define FIX2INT(x) rb_fix2int((VALUE)x) #define FIXNUM_P(f) (((long)(f))&FIXNUM_FLAG)
Fixnum 类表示小整数,由 INT2FIX 得到。由于可能在程序中频繁使用,将其用结构体表示可能会使执行速度大打折扣,因此直接嵌入 VALUE 中。数值左移一位或上0x01使得 Fixnum总是一个奇数,其实际可用比特位为 sizeof(long)*8-1 ,可表示的最大最小值分别为FIXNUM_MAX 和 FIXNUM_MIN ,超出该范围的数属于 Bignum 类,由C结构体实现。
宏 FIXNUM_P 返回一个布尔值,P 表示断言(predicate),即“参数f是否为 fixnum 对象”。FIX2INT 将 Fixnum 转换回来,由 rb_fix2int 函数实现,它根据实际平台上 int 和long 的大小是否相同来分别处理。
注意,在 VALUE 中 Fixnum 总是一个奇数,而使用 C 结构体表示的 Ruby 对象的内存空间是由 malloc 分配的,所得地址通常为4的倍数,因此以VALUE表示的 Fixnum 和C结构体 Ruby对象不会发生冲突。
Symbol(ruby.h)
typedef unsigned long ID; #define SYMBOL_FLAG 0x0e #define SYMBOL_P(x) (((VALUE)(x)&0xff)==SYMBOL_FLAG) #define ID2SYM(x) ((VALUE)(((long)(x))<<8|SYMBOL_FLAG)) #define SYM2ID(x) RSHIFT((unsigned long)x,8)
Ruby 的 Symbol 定义很简单,它在C层次上就是一个无符号整数,转换为 Ruby 的 VALUE值的方法是先左移8位,然后加上 Symbol 的 flag 0x0e,这使得 Symbol 既不是4的倍数,也不是奇数,以 VALUE 表示的 Symbol 不会和 fixnum 对象,结构体对象冲突。
True false nil undef(ruby.h)
#define Qfalse ((VALUE)0) /*false*/ #define Qtrue ((VALUE)2) /*true*/ #define Qnil ((VALUE)4) /*nil*/ #define Qundef ((VALUE)6) /* undefined value for placeholder */
Ruby 中一切皆对象,连 true/false/nil 也是。由于虚拟内存的第一个块(对应地址4)一般不分配, Qnil 也不会和结构体对象冲突。
这样以 VALUE 表示的C结构体对象、Fixnum、Symbol、true、false、nil和undef在内存地址空间中都可以互不侵犯,和平共处。 VALUE 可以引用 Ruby 内部所有的对象,这使得 Ruby可以统一处理不同类型的对象。
当给定一个 VALUE 对象时,使用 TYPE 宏判断该对象的种类,例如 Symbol 对象类型为T_SYMBOL。
TYPE宏(ruby.h)
#define T_MASK 0x3f #define BUILTIN_TYPE(x) (((struct RBasic*)(x))->flags & T_MASK) static inline int rb_type(obj) VALUE obj; { if (FIXNUM_P(obj)) return T_FIXNUM; if (obj == Qnil) return T_NIL; if (obj == Qfalse) return T_FALSE; if (obj == Qtrue) return T_TRUE; if (obj == Qundef) return T_UNDEF; if (SYMBOL_P(obj)) return T_SYMBOL; return BUILTIN_TYPE(obj); } #define TYPE(x) rb_type((VALUE)(x))
inline 函数 rb_type 首先判断 VALUE 是否为内嵌对象,即 Fixnum、Symbol、true、false、nil和undef 。如果都不是,说明该对象是一个C结构体对象,调用 BUILTIN_TYPE 宏二次判断。该宏涉及到结构体对象中的内容。
结构体对象例子: String
我们以 String 对象定义为例看一下 Ruby 结构体对象。
String(ruby.h)
struct RBasic { unsigned long flags; VALUE klass; }; struct RString { struct RBasic basic; long len; char *ptr; union { long capa; VALUE shared; } aux; }; #define RSTRING_PTR(s) (RSTRING(s)->ptr) #define RSTRING_LEN(s) (RSTRING(s)->len)
和我们自己在C中使用字符串时差不多,最重要的两个成员是 len 和 ptr 。 len 表示字符串长度, ptr 指向实际的字符数组。 RSTRING_PTR 和 RSTING_LEN 用来快速访问这两个域,当然使用之前最好用 TYPE 宏检查一下 VALUE s 是否真正是一个 String 对象。 aux联合涉及 String 对象处理技巧,这里我们不讨论。
注意 RString 定义中有一个类型为 RBasic 结构体的成员,里面是 flags 和 klass ,联系上面 TYPE 宏的定义, BUILTIN_TYPE 宏要检查这个 flags 值:没错, flags 就存储着结构体对象的类型。当然 T_MASK 只占用了 unsigned long 中的6个比特位,剩下的 flags 位中还有一些用于其他用途。
另一个 RBasic 成员 klass 是 VALUE 类型,指向这个对象归属的类,即“类对象”。在Ruby 中类也是以对象存在的(记住,一切皆对象)。类对象用 struct RClass 结构体表示,对应的 TYPE 标志为 T_CLASS 。
除个别结构体外,绝大多数 Ruby 结构体对象第一个域为 RBasic 成员,这使得 Ruby 可以一致地判断各种对象的类型。
此外还有一个 CLASS_OF 宏,用来得到对象的归属类对象,由 inline 函数 rb_class_of实现,对于结构体对象返回 RBasic 成员的 klass 的值,对于 VALUE 内嵌对象,他们没有结构体,则返回 Ruby 初始化时创建的类对象指针,例如 Symbol 类对象指针为rb_cSymbol 。
CLASS_OF 宏(ruby.h)
static inline VALUE rb_class_of(obj) VALUE obj; { if (FIXNUM_P(obj)) return rb_cFixnum; if (obj == Qnil) return rb_cNilClass; if (obj == Qfalse) return rb_cFalseClass; if (obj == Qtrue) return rb_cTrueClass; if (SYMBOL_P(obj)) return rb_cSymbol; return RBASIC(obj)->klass; } #define CLASS_OF(v) rb_class_of((VALUE)(v))
可以看到, Symbol 对象和 String 对象在 Ruby 内部的表示完全不同。 Symbol 对象直接嵌入到 VALUE 中,本质上是一个数字,这个数字和创建 Symbol 的名字形成一对一的映射;而String 对象是一个重量级的用C结构体表示的家伙,因此使用 Symbol 和 String 的开销相差很大。
那么 Symbol 对象的数字 ID 和名字是如何对应起来的呢?下面我们就会看到。
符号表(Symbol Table)
我们使用:<字符串>的方式创建 Symbol 对象,但到现在为止,我们看到的 Symbol 只是一个数字,那这个数字对应的名字在哪里?在符号表里。
符号表是一个全局数据结构,它存放了所有 Symbol 的(数字ID,名字)对, Ruby 不会从中删除 Symbol ,因此当你创建一个 Symbol 对象后,它将一直存在,直到程序结束。为了方便 name 和 ID 之间的双向查找,设置了两个符号表 sym_tbl 和 sym_rev_tbl 。
Symbol table(parse.c)
static st_table *sym_tbl; /*name to ID*/ static st_table *sym_rev_tbl; /*ID to name*/ void Init_sym() { sym_tbl = st_init_strtable_with_size(200); sym_rev_tbl = st_init_numtable_with_size(200); }
符号表采用链式哈希表,如下图所示。
bins 为数组指针,每个数组元素类型为 struct st_table_entry * 指针,通过哈希表元素的 next 成员链成链表。 num_bins 为数组元素个数, num_entries 为哈希表元素个数。每个哈希表元素包括哈希值和 key/record 对。
链式哈希表
哈希表定义
/*st.h*/ typedef struct st_table st_table; struct st_hash_type { int (*compare)(); int (*hash)(); }; struct st_table { struct st_hash_type *type; int num_bins; int num_entries; struct st_table_entry **bins; }; typedef unsigned long st_data_t; /*st.c*/ typedef struct st_table_entry st_table_entry; struct st_table_entry { unsigned int hash; st_data_t key; st_data_t record; st_table_entry *next; };
st_table 的 type 成员存放该哈希表的 key 比较函数指针和哈希函数指针,在哈希表初始化时填入。例如,对于 ID 到 name 的 sym_rev_tbl 表,其哈希函数就是简单地返回ID。
Symbol 方法实现
有了数据结构,我们来看算法:对象方法的实现。
类对象初始化
在 Ruby 中类也是以对象存在的,即类对象。对象通过 CLASS_OF 宏可以得到它的归属类对象,对象的方法就存储在类对象的方法表中,也是一个哈希表。类对象初始化的时候通过调用rb_define_method 将对象方法加入到方法表中。
Symbol类对象初始化(object.c)
VALUE rb_cSymbol; /*Symbol class object, global variable*/ /*class name:Symbol, super class:rb_cObject*/ rb_cSymbol = rb_define_class("Symbol", rb_cObject); /*method initialization*/ … rb_define_method(rb_cSymbol, "to_s", sym_to_s, 0); rb_define_method(rb_cSymbol, "id2name", sym_to_s, 0);
String类对象初始化(String.c)
VALUE rb_cString; /*class name:String, super class:rb_cObject*/ rb_cString = rb_define_class("String", rb_cObject); /* method initialization*/ … rb_define_method(rb_cString, "intern", rb_str_intern, 0); rb_define_method(rb_cString, "to_sym", rb_str_intern, 0);
显然, String 对象的 intern 方法和 to_sym 方法由 rb_str_intern 函数实现,而Symbol 对象的 to_s 和 id2name 方法由 sym_to_s 函数实现。
String 到 Symbol 的方法
在 Ruby 内部, String 对象到 Symbol的转换由 string.c 中的函数 rb_str_intern实现。
该函数进行一些必要的检查,然后调用 rb_intern 函数创建字符串和 ID 的映射。该函数很有意思,它实际上是 Ruby 解析器的一部分,从中我们可以看到 Ruby 是如何处理程序中的各种名字符号的。
函数 rb_intern 大致完成以下工作:
- 搜索符号表,如果名字对应的 Symbol 已经创建,直接返回 symbol 的 ID。
- 解析名字的类型(全局变量、类变量、实例变量、操作符、属性赋值( attribute assignment )、局部变量、常量或其他),创建 ID 并打上相应的类型标记(在 ID 的最低3位),其中 Ruby 操作符的 ID 是在 op_tbl 表中预定义的,其他的由全局变量 last_id 动态生成。这样每一个名字都将唯一对应一个 ID。
- 将(name,ID)对登记在符号表 sym_tbl 和 sym_rev_tbl 中。
简化的 rb_intern 函数(parse.c)
ID rb_intern(name) const char *name; { const char *m = name; ID id; int last; if (st_lookup(sym_tbl, (st_data_t)name, (st_data_t *)&id)) return id; last = strlen(name)-1; id = 0; /*...parse the name, tag attribute for id*/ id_regist: name = strdup(name); st_add_direct(sym_tbl, (st_data_t)name, id); st_add_direct(sym_rev_tbl, id, (st_data_t)name); return id; }
最终 rb_intern 返回的 ID 被 rb_str_intern 转换为一个VALUE值(通过宏ID2SYM)
Symbol 到 String 的方法
Symbol 到 String 由 sym_to_s 实现。其中 rb_id2name 函数将 id 转换为一个名字,然后 rb_str_new2 使用该名字创建一个新的 string 对象。
sym_to_s 函数(object.c)
static VALUE sym_to_s(sym) VALUE sym; { return rb_str_new2(rb_id2name(SYM2ID(sym))); }
函数 rb_id2name 本质上是简单地查询操作符表 op_tbl 和符号表 sym_rev_tbl ,但由于属性赋值类型名字处理的缘故,具体代码要稍复杂一些。
Ruby 1.9 中的新变化
就在本文写作过程中,2007 年圣诞节, Ruby 1.9 发布了。
在 Ruby 1.9 中, Symbol 有了一些变化,使得 Symbol 看起来“更像” String 了。比如Symbol 的===方法,在 Ruby 1.8.6 和 Ruby 1.9 中是完全不同的。
symbol===string
irb(main):001:0> [RUBY_VERSION, RUBY_RELEASE_DATE] => ["1.8.6", "2007-09-24"] irb(main):002:0> :a === "a" => false irb(main):003:0> irb(main):001:0> [RUBY_VERSION, RUBY_RELEASE_DATE] => ["1.9.0", "2007-12-25"] irb(main):002:0> :a === "a" => true irb(main):003:0>
除此之外,新的 Symbol 类包含了比较模块,使得 Symbol 对象可以使用诸如”==”、”>=”、”<=”等比较操作符。
Symbol 比较
irb(main):001:0> :test1 >= :test2 => false irb(main):002:0> :test3 >= :test2 => true irb(main):003:0>
Symbol 类还定义了以前只有 String 类才有的方法,比如,[]、casecmp、length、capitalize等等。
在实现上, Symbol 类对象和 string 类对象的初始化都放到了 string.c 中,这似乎也使得 Symbol 和 String 拉近了距离。
难道 Symbol 和 String 变成一个东西了?没有。类的关系没有变, Symbol 的父类还是Object , Symbol 的表示和存储也没有变。新方法的实现只是利用了 Symbol 和 String的互换函数。比如 Symbol 的 capitalize 方法就是将 Symbol 转换为 String ,最后再转换回来。这个时候其实已经是一个新的 Symbol 了。
capitalize 方法
static VALUE sym_capitalize(VALUE sym) { return rb_str_intern(rb_str_capitalize(rb_id2str(SYM2ID(sym)))); }
当然可能还会有新的变化。不过我个人还是希望 Symbol 能保持简单,因为它本来就不复杂。而复杂的东西往往最终会失去生命力。
小结
Symbol 的特点决定了它的简洁和高效。 Ruby 把它的符号表敞开了给你,在 Symbol 可以更好地发挥作用时,不要吝啬使用 Symbol 。
转自:http://www.ibm.com/developerworks/cn/opensource/os-cn-rubysbl2/index.html