在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
Ruby 是一个强大的面向对象脚本语言(本文所用 Ruby 版本为1.8.6),在 Ruby 中 Symbol 表示“名字”,比如字符串的名字,标识符的名字。 创建一个 Symbol 对象的方法是在名字或者字符串前面加上冒号:
你可能会问,字符串就是字符串,干吗还有字符串的名字?这是因为在 Ruby 中字符串也是一种对象,即 String 对象。无论其结构还是操作和 Symbol 对象都是不同的。 在 Ruby 中每一个对象都有唯一的对象标识符(Object Identifier),可以通过 object_id 方法来得到一个对象的标识符。我们来看看 Symbol 对象和 String 对象的差别:
可以看到,前三行语句中的 :foo (或者 :"foo")都是同一个 Symbol 对象,其 object id 为327458,而后三行中的字符串”foo”都是不同的对象,其 object id 依次为24303850、24300010、24296170。 可见,每个 String 对象都是不同的,即便他们包含了相同的字符串内容;而对于 Symbol 对象,一个名字(字符串内容)唯一确定一个 Symbol 对象。 值得注意的是创建 Symbol 对象的字符串中不能含有’\0’字符,而 String 对象是可以的。
除了可以采用一般的字符串,还可以使用操作符(例如+, -, *, /),变量,常量,方法甚至类的名字来创建 Symbol 对象,例如:+就是一个合法的 Symbol 。实际上,在 Ruby 内部操作符、变量等名字本身就是作为 Symbol 处理的,例如当你定义一个实例变量时, Ruby 会自动创建一个 Symbol 对象,例如 @test 对应为 :@test 。
这个类定义了一个具有读写方法的实例变量 @test 。实际上 Ruby 创建了两个 Symbol ,一个是实例变量的 symbol :@test ,另一个是 :test 。那如果使用字符串对象 ”test” 作为参数呢?也可以,仍然会创建两个 symbol ,:test 和 :@test ,为什么还会创建 :test 呢?这是和Ruby的实现相关的(至少Ruby1.8.6里是这样)。 注意,类变量 @@test 和实例变量 @test 对应的 Symbol 显然是不同的。记住:名字相同,则Symbol 相同。
第一个例子里,类名、常量名和方法名都是 Test ,因此相应的 Symbol 对象都是 :Test 。不用担心, Ruby 可以很好区分它在不同上下文中到底表示什么。当然这并不是一个好的编程风格,但对于理解 Ruby 的 Symbol 还是有帮助的: Symbol 表示一个名字,仅此而已。 Symbol 对象一旦定义将一直存在,直到程序执行退出。所有 Symbol 对象存放在 Ruby 内部的符号表中,可以通过类方法
Symbol 对象和 String 对象是完全不同的东西,对象标识符很明确的说明了这一点。除此之外,我们还可以从两种对象的方法上区分。 查看 Ruby 库参考,你会发现 String 类有非常多的方法,包括 Mixed-in 方法(Ruby中一个类通过 include 其他模块而得到的方法,实现多重继承的效果)、类方法和实例方法;而 Symbol 类只有一个类方法 例如,可以通过
虽然 Symbol 和 String 是不同的对象,但它们之间关系很密切。 Ruby 提供了方法在 Symbol和 String 之间转换。 使用 to_s 或 id2name 方法将 Symbol 转化为一个 String 对象:
注意,每个 String 对象都是唯一的,因此对一个 Symbol 调用多次将产生多个 String 对象。 除了在字符串前面加冒号,还可以使用 to_sym 或 intern 方法将 String 转化为 Symbol ,如果该 Symbol 已经存在,则直接返回。
正如前边提到的, Ruby 内部一直在使用 Symbol ,比如 Ruby 程序中的各种名字,Symbol本质上是 Ruby 符号表中的东西。使用 Symbol 处理名字可以降低 Ruby 内存消耗,提高执行速度,这点我们在下一篇文章中会看到。 那么 Symbol 对我们有什么用呢?当然也是内存。使用 String 的开销太大了,因为每一个String 都是一个对象。想想前边的例子,一个字符串每出现一次 Ruby 就会创建一个 String 对象。 通常来讲,当你面临 String 还是 Symbol 的选择时,可以参考以下标准:
那么什么时候我们会用到名字呢?很多时候都会,比如枚举值、关键字(哈希表关键字、方法的参数)等等 哈希表是 Symbol 应用最为广泛的地方。 在ruby中,哈希和数组类似,一个哈希表是一系列 key/value 对的集合,只不过它的 key 取值范围更广泛,可以是任何对象,比如正则表达式。但通常我们都会取有意义的 key ,比如 String、Symbol 。 下面这个哈希表表示按城市分类的一些机器的集合。
如果要引用 beijing 的机器,使用 hosts['beijing'] 。但如果我们程序中要频繁引用哈希表中 value ,这样就不大好了,因为 Ruby 对每一次字符串引用都会生成一个 String 对象,累积下来这个开销是相当大的。
哈希参数 通常我们定义的函数的参数的个数和顺序是写死的,调用函数的时候要确保参数的个数、顺序匹配,有时候这样很不方便,使用哈希参数可以解决这个问题。 ROR 中就大量地运用这种方式,也许你已经看到了,到处都是 Symbol 和哈希。比如:
使用哈希参数的方法可以如下定义,前半部分为固定参数,后面为可变参数,或者干脆全采用哈希参数:
如果你希望设定一些默认参数,并允许调用者更改这些参数,可以使用哈希对象的 merge! 方法
在 Rails 中,对 hash 类进行了扩展,可以使用 reverse_merge! 方法来达到上述效果。该方法在 ActiveSupport::CoreExtensions::Hash::ReverseMerge 中定义。Rails 甚至还提供了 assert_valid_keys 方法,对传递进来的哈希表的 keys 进行合法性检验。 Perl 说,条条大路通罗马。在 Ruby 中也是这样的,也许你会发现更好的应用 Symbol 和哈希的方法。 在上一篇《理解 Ruby Symbol ,第 1 部分:使用 Symbol 》中,我们大致了解了 Symbol,包括 Symbol 和 String 的区别、 Symbol 的使用等。本文我们将深入到 Ruby 内部去看看 Symbol 的样子,这对更好理解和使用 Symbol 可能会有些帮助。 Ruby 是用 C 语言实现的,本文我们将以 Ruby 1.8.6 版本实现为例。 Ruby 是一个完全面向对象的语言,这点很好地体现在对象方法调用的一致性上,一个对象,无论是如何创建的,调用实例方法的方式是一样的,即“<对象>.<方法名>”。
自然 Symbol 也是一种对象,那么 Ruby 内部是如何一致地表示各种对象的呢?
没错,这就是 Ruby 对象的定义,确切地说是对象的引用。在 Ruby 中绝大多数对象的内容表示为 C 语言中的结构体,比如用户定义的对象和 Ruby 预定义的对象如 String, Array 等。我们知道引用结构体的方法是指针, 这是因为Ruby在 VALUE 中内嵌了其他类型的,不用结构体表示的对象,也就是说直接用VALUE 表示的对象,这其中就有 Symbol 。考虑到现在大多数计算机体系上 内嵌到 VALUE 中的对象有 Fixnum,Symbol,true,false,nil 和 undef 。
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对象不会发生冲突。
Ruby 的 Symbol 定义很简单,它在C层次上就是一个无符号整数,转换为 Ruby 的 VALUE值的方法是先左移8位,然后加上 Symbol 的 flag 0x0e,这使得 Symbol 既不是4的倍数,也不是奇数,以 VALUE 表示的 Symbol 不会和 fixnum 对象,结构体对象冲突。
Ruby 中一切皆对象,连 true/false/nil 也是。由于虚拟内存的第一个块(对应地址4)一般不分配, Qnil 也不会和结构体对象冲突。 这样以 VALUE 表示的C结构体对象、Fixnum、Symbol、true、false、nil和undef在内存地址空间中都可以互不侵犯,和平共处。 VALUE 可以引用 Ruby 内部所有的对象,这使得 Ruby可以统一处理不同类型的对象。 当给定一个 VALUE 对象时,使用 TYPE 宏判断该对象的种类,例如 Symbol 对象类型为T_SYMBOL。
inline 函数 rb_type 首先判断 VALUE 是否为内嵌对象,即 Fixnum、Symbol、true、false、nil和undef 。如果都不是,说明该对象是一个C结构体对象,调用 BUILTIN_TYPE 宏二次判断。该宏涉及到结构体对象中的内容。 我们以 String 对象定义为例看一下 Ruby 结构体对象。
和我们自己在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 。
可以看到, Symbol 对象和 String 对象在 Ruby 内部的表示完全不同。 Symbol 对象直接嵌入到 VALUE 中,本质上是一个数字,这个数字和创建 Symbol 的名字形成一对一的映射;而String 对象是一个重量级的用C结构体表示的家伙,因此使用 Symbol 和 String 的开销相差很大。 那么 Symbol 对象的数字 ID 和名字是如何对应起来的呢?下面我们就会看到。 我们使用:<字符串>的方式创建 Symbol 对象,但到现在为止,我们看到的 Symbol 只是一个数字,那这个数字对应的名字在哪里?在符号表里。 符号表是一个全局数据结构,它存放了所有 Symbol 的(数字ID,名字)对, Ruby 不会从中删除 Symbol ,因此当你创建一个 Symbol 对象后,它将一直存在,直到程序结束。为了方便 name 和 ID 之间的双向查找,设置了两个符号表 sym_tbl 和 sym_rev_tbl 。
符号表采用链式哈希表,如下图所示。 bins 为数组指针,每个数组元素类型为 struct st_table_entry * 指针,通过哈希表元素的 next 成员链成链表。 num_bins 为数组元素个数, num_entries 为哈希表元素个数。每个哈希表元素包括哈希值和 key/record 对。
st_table 的 type 成员存放该哈希表的 key 比较函数指针和哈希函数指针,在哈希表初始化时填入。例如,对于 ID 到 name 的 sym_rev_tbl 表,其哈希函数就是简单地返回ID。 有了数据结构,我们来看算法:对象方法的实现。 在 Ruby 中类也是以对象存在的,即类对象。对象通过 CLASS_OF 宏可以得到它的归属类对象,对象的方法就存储在类对象的方法表中,也是一个哈希表。类对象初始化的时候通过调用rb_define_method 将对象方法加入到方法表中。
显然, String 对象的 intern 方法和 to_sym 方法由 rb_str_intern 函数实现,而Symbol 对象的 to_s 和 id2name 方法由 sym_to_s 函数实现。 在 Ruby 内部, String 对象到 Symbol的转换由 string.c 中的函数 rb_str_intern实现。 该函数进行一些必要的检查,然后调用 rb_intern 函数创建字符串和 ID 的映射。该函数很有意思,它实际上是 Ruby 解析器的一部分,从中我们可以看到 Ruby 是如何处理程序中的各种名字符号的。 函数 rb_intern 大致完成以下工作:
最终 rb_intern 返回的 ID 被 rb_str_intern 转换为一个VALUE值(通过宏ID2SYM) Symbol 到 String 由 sym_to_s 实现。其中 rb_id2name 函数将 id 转换为一个名字,然后 rb_str_new2 使用该名字创建一个新的 string 对象。
函数 rb_id2name 本质上是简单地查询操作符表 op_tbl 和符号表 sym_rev_tbl ,但由于属性赋值类型名字处理的缘故,具体代码要稍复杂一些。 就在本文写作过程中,2007 年圣诞节, Ruby 1.9 发布了。 在 Ruby 1.9 中, Symbol 有了一些变化,使得 Symbol 看起来“更像” String 了。比如Symbol 的===方法,在 Ruby 1.8.6 和 Ruby 1.9 中是完全不同的。
除此之外,新的 Symbol 类包含了比较模块,使得 Symbol 对象可以使用诸如”==”、”>=”、”<=”等比较操作符。
Symbol 类还定义了以前只有 String 类才有的方法,比如,[]、casecmp、length、capitalize等等。 在实现上, Symbol 类对象和 string 类对象的初始化都放到了 string.c 中,这似乎也使得 Symbol 和 String 拉近了距离。 难道 Symbol 和 String 变成一个东西了?没有。类的关系没有变, Symbol 的父类还是Object , Symbol 的表示和存储也没有变。新方法的实现只是利用了 Symbol 和 String的互换函数。比如 Symbol 的 capitalize 方法就是将 Symbol 转换为 String ,最后再转换回来。这个时候其实已经是一个新的 Symbol 了。
当然可能还会有新的变化。不过我个人还是希望 Symbol 能保持简单,因为它本来就不复杂。而复杂的东西往往最终会失去生命力。 Symbol 的特点决定了它的简洁和高效。 Ruby 把它的符号表敞开了给你,在 Symbol 可以更好地发挥作用时,不要吝啬使用 Symbol 。
田强,任职于 IBM 中国软件开发中心 Tivoli 部门,喜欢开源和 Linux 转自:http://www.ibm.com/developerworks/cn/opensource/os-cn-rubysbl/index.html |
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论