-
2007-05-22
ID和Symbol(二)
从抽象的角度来说,C和Java相比有很多不尽如人意的地方,它的数据和方法不能像Java那样封装在一起。但是,某些地方它的抽象可以做得比Java更好,比如typedef。如果我们要面对的是一个复杂类型,Java的优势尽显无疑,但是如果我们面对的是一个简单的类型呢?比如,只是一个long类型。我们这里要讨论的ID和Symbol就面临着这种问题。
之前,我们已经见过了Ruby中对应ID的定义:
typedef unsigned long ID;
(ruby.h)
因为typedef这种方法的存在,让我们拥有一个不同的名称,可以很好的完成抽象,又不必像普通的对象那样在生成对象上付出很多,一举两得。《整数对象》实际上就是讨论了这样一个问题。
如何对应到Java中呢?除非我们想放弃抽象,否则,我们多半是有一个类承担这样的工作。在XRuby的实现中,ID对应着RubyID。
package com.xruby.runtime.lang;
public class RubyID {
private long id;
...
}
(RubyID.java)
说到这里问题来了!在前面的讨论中,我们知道ID是一个整数,Symbol实际上也是一个整数。那么我们是不是应该按照同样的方法设计一个RubySymbol呢?不妨顺着这个思路想下去,看看这种设计会带来什么。
我们已经知道了Ruby中ID和Symbol是一一对应的,那么在这里,我们也应该让ID和Symbol一一对应,这一点很容易做到,只要让它们的整数成员存在某种对应关系便可。另外,我们也知道,ID和Symbol的存在就是为了提高字符串比较的性能,所以,我们也希望在ID和Symbol的实现中,比较的性能也尽可能高。在Java中来得最快的比较莫过于直接比较引用了,所以,如果ID能够做到在系统内唯一,我们就可以用直接比较ID来代替比较其中成员,而且这种实现还可以省去每次都创建对象的成本,一举两得。显然,既然提出了这个问题,那么Symbol也应该有对应的实现。但是,如果这样做的话,怎样保证ID和Symbol之间的一一对应呢?换句话说,怎么实现ID和Symbol之间的转换呢?
有了字符串到ID的映射,很自然的会联想到用hash表实现这个功能。但问题也随之而来,我们用ID和Symbol本来是为了提升性能的,但它们之间的转换却加入了hash表的操作,于是Symbol转换为Ruby字符串的操作成本将大为提升,而且,我们还要在为增加的双向转换的两个Hash表负责。虽然这样可以实现,但这种方法会让人如鲠在喉。
当然,我们可以放弃ID和Symbol在系统内的唯一性来做到这一点,让每次转换生成一个新的ID或是Symbol,基于我们前面讨论的内容,这样做会动摇它们存在的根基。
其实,解决办法很简单,既然二者之间是个一一对应的关系,不妨就利用这种关系:
package com.xruby.runtime.lang;
public class RubyID {
private long id;
private RubySymbol symbol;
...
}
(RubyID.java)
package com.xruby.runtime.lang;
public class RubySymbol extends RubyValue {
private RubyID id;
...
}
(RubySymbol.java)
这样,二者之间的转换也很容易完成:
public RubySymbol toSymbol() {
if (this.symbol == null) {
this.symbol = new RubySymbol(this);
}
return this.symbol;
}
(RubyID.java)
public RubyID toID() {
return this.id;
}
(RubySymbol.java)
至于Symbol中的整数,很容易通过RubyID中的成员转换出来。
其实,做到这里,可以在进一步考虑一下,原来的long是否可以省略呢?毕竟RubyID拥有两个成员,不是一件令人愉快的事情。而且,在RubyID和RubySymbol的直接对应中,这个long成员并没有起到什么作用。而在RubyID与字符串的对应中,也没有用到那个long成员。在C Ruby的实现中,在它生成的过程中,加入了一些相关信息,比如全局变量、实例变量等等。为了保持兼容,暂时还是保留了这个成员。 -
2007-05-21
ID和Symbol(一)
ID和Symbol是Ruby中同字符串相关的两个东西,他们的存在是为了一个共同的目的:替代字符串。因为字符串的比较通常代价不菲,以ID和Symbol代替字符串可以提高字符串比较的性能,所以,用到字符串比较的地方都可以选择用ID或是Symbol作为替代品,比如,做Hash表的键值。不同的是,ID用在C层次,而Symbol用在Ruby层次。
下面是ID的定义:
typedef unsigned long ID;
(ruby.h)
至于Symbol,我在《管窥Ruby——对象基础》说过,在Ruby实现中,VALUE是所有Ruby对象的根基,Symbol也不例外。不同于字符串等类型,我们可能并不会在Ruby实现的源码中看到Symbol类型的定义,它走了与整数类似的一条路,也就是说,它是内嵌在VALUE中的。下面是判断一个VALUE是否为SYMBOL的方法。
#define SYMBOL_FLAG 0x0e
#define SYMBOL_P(x) (((VALUE)(x)&0xff)==SYMBOL_FLAG)
(ruby.h)
从上面的两段代码,我们不难发现,ID和Symbol实际上都是一个整数。它们都唯一对应着一个字符串,不同的是,ID对应着C的字符串,Symbol对应着Ruby的字符串。既然它们都对应着字符串,那么,它们之间便也顺理成章的存在着一些对应关系。事实上,它们之间的距离比我们想象的还要近,在Ruby的实现中,它们之间是可以直接转换的。
#define ID2SYM(x) ((VALUE)(((long)(x))<<8|SYMBOL_FLAG))
#define SYM2ID(x) RSHIFT((unsigned long)x,8)
这里,RSHIFT是一个平台相关的右移实现。从这里我们不难发现,ID和Symbol之间就是一一对应的。
那它们是如何对应到字符串的呢?ID和C字符串之间的对应是由下面两个函数完成的:
ID rb_intern(const char *name);
char* rb_id2name(ID id);
(parse.c)
其中,rb_inern完成C字符串到ID的映射,rb_id2name完成ID到C字符串的映射。其实现很简单,基本上就是一个在hash表中查找,如果没有,便创建一个。
那么Symbol和Ruby字符串是如何对应的呢?看一下Symbol的to_s方法就知道了。这个方法是这样定义出来的。
rb_define_method(rb_cSymbol, "to_s", sym_to_s, 0);
(object.c)
它实现于sym_to_s方法中。
static VALUE
sym_to_s(sym)
VALUE sym;
{
return rb_str_new2(rb_id2name(SYM2ID(sym)));
}
(object.c)
看到了吧!先由Symbol转成ID,再由ID对应到C字符串,最后由C字符串生成Ruby的字符串。绕了好大的一个圈子,所以,Symbol与其对应的Ruby字符串距离还真的是不近。所以,我前面会说,ID和Symbol之间的距离比想象得要近。 -
2007-05-14
添加方法的小魔术
Ruby中有很多钩子,可以用来监控系统中的各种事件,比如,添加一个新的方法时,会调用类的method_added方法,下面是一段示例代码:
class A
def A.method_added symbol
p symbol
end
def m
end
end
可以看到,因为这个方法定义了一个method_added的钩子,随后定义的方法时就会调用它,因此,虽然看上去没有什么具体的执行代码,这段代码依然就会产生输出。
在Ruby的实现中,定义方法是由rb_define_method实现的,这个方法最终会调用rb_add_method,而添加方法的钩子就是在这个方法中实现的。
void
rb_add_method(klass, mid, node, noex)
VALUE klass;
ID mid;
NODE *node;
int noex;
{
...
rb_funcall(klass, added, 1, ID2SYM(mid));
...
}
(eval.c)
这里的added实际上就是“method_added“这个字符串对应的ID,在之前的blog中提到过,Ruby的实现ID和字符串是一一对应的。所以,这段代码就相当于调用类的method_added方法。Module类实现了一个缺省的method_added方法(什么都不做),Class类继承自Module类,所以,它也拥有了method_added方法。
不过,仔细查看Ruby的源码,我们会发现,method_added并不是我们定义的第一个方法,这样就带来一个问题,那就是在没有这个方法定义未完成之前,那个调用会不会出错。事实上,即便它是我们定义的第一个方法,也会有同样的问题,因为在定义method_added方法时,method_added方法并没有完成定义,有些绕,但情况就是这样。事实会告诉我们,Ruby活得不错,没有在初始化的时候就告诉我们method_added方法未定义。
代码是说明问题的最好办法,其实,我们之前拿出来的代码漏掉了一些东西,那段代码前后的部分大概是这样:
if (node && mid != ID_ALLOCATOR && ruby_running) {
if (FL_TEST(klass, FL_SINGLETON)) {
rb_funcall(rb_iv_get(klass, "__attached__"), singleton_added, 1, ID2SYM(mid));
}
else {
rb_funcall(klass, added, 1, ID2SYM(mid));
}
}
(eval.c)
问题就在ruby_running,这是一个Ruby是否正在运行的标识。它的初值是0,
static int ruby_running = 0;
(eval.c)
把它赋值为1的地方是在ruby_init中,
void
ruby_init()
{
...
ruby_running = 1
}
(eval.c)
看到了吧!只有当初始化完成之后,它才会设置ruby_running,而一些builtin方法的定义是在初始化过程中完成的,所以,它们是可以正常完成定义的,当然,这也依赖于method_added缺省实现的无所作为。
其实,稍微查看一下,我们不难发现除了用于方法添加过程之外,ruby_running这个变量还在方法缓存中起作用,比如包含一个模块的时候,会清空cache,
void
rb_include_module(klass, module)
VALUE klass, module;
{
...
if (changed) rb_clear_cache();
}
(class.c)
而我们知道,方法缓存只有在有方法调用之后,才会有内容,在初始化的过程中,这个操作显然有些浪费,所以,在清理缓存之前,稍微检查一下,就可以省去每次都做的苦恼,对于提高初始化的性能,很有帮助。
void
rb_clear_cache()
{
...
if (!ruby_running) return;
...
}
(eval.c)
其实,ruby_running只是一个简单标识,这是编程中很常用的一种方法,表示一件事情的完成,没有什么难度。只是理解添加方法这个问题时,可能会一不小心走到误区里面,正如我前面提到的那个有些绕的说法。 -
2007-05-08
踏上新征程
当我登上去往西安的飞机时,20多天的“长假”结束了。这是我工作以来休息最长的一次,之所以用了引号,因为这段时间,我是一个无工作的人,谈不上放假。
这个“假”休得比上班还累。因为办理离职的原因,我的大多数时间是在跑来跑去的,而且难得回到公司,朋友很多,忙活了一白天,晚上还要和朋友们一起聚聚。好些次都累到躺在床上一点都不想动。和人事的人聊天时,大家一致认定办理入职手续的时候,从来没有如此麻烦过,由此推断,公司是不鼓励离职的。:)
曾经在西安读书,这次到西安,被许多朋友戏称为回到老巢。迈出机舱的那一刻,呼吸着西安水气十足的空气,可怜一早从沈阳出发的我穿了一件外套,汗一下子就冒了出来,突然有一种桑拿的感觉,这是典型的夏天西安的天气。西安,我又回来了!
我不喜欢西安夏天的热,当年因为数学建模培训的原因,曾经在西安体会过夏天最热的时候,真的是让我饱受折磨,常有同学睡到半夜热得起来冲凉。我非常喜欢西安的小吃,好吃又便宜,而且种类繁多,念书的时候,学校周围的小吃几乎吃了个遍,那时候还不觉得,毕业离开西安之后,我才觉得,还是西安的小吃好,这也是我这次回到西安的一件主要的编外任务。
不同于上次到西安面试的匆匆忙忙,这次要在西安待上一段时间。同样不同于上次到西安面试,这次 ThoughtWorks搬了一个新的大厅,至少比之前的的大厅多放上一张大桌子,显然人更多了。至于其它空间——比如会议室——的增加,我倒并不是太在意,只是注意到厨房已经单独独立了出来,游戏机也在里面,适合边吃边打。
我到的时候,正好的是下午刚开始上班的工作,趁着大家还没进入到工作状态之中,和大家打了招呼。说实话,人太多,有些记不住,好在墙上贴满了大家的照片,闲暇时可以看看,尽快熟悉同事。有一件很有趣的事,一个同事的名字和我的名字的读音刚好反过来,在英文的交流环境下,有人把名放在前面,有人把姓放在前面,我们俩的名字足以给大家带来相当的麻烦,据说,我还没入职,已经有人开始混淆了。
下午领了机器,一台Dell的双核笔记本,稍微值得吹毛求疵的地方是,宽屏,按我的说法,适合看电影,对于开发的好处,我还没有体会到。给自己装了个双系统,终于可以用Ubuntu了,之前就是因为宽带的原因,我一直没法使用Ubuntu,现在有机会了,争取让 Ubuntu成为自己的日常开发环境,显然周围的Ubuntu Fans不少。我一直没搞清楚,在大厅里随便坐,那网络的问题如何解决,到自己装机的时候才知道,原来用的都是无线网络。今天我蹲着装机器曾引起了别人的同情,有了这样的网络环境,哪天可以尝试一下趴在地上,呵呵,这可是我在自己的住处经常使用的一种开发造型啊!
新环境,新征程,起步了! -
2007-04-25
加入ThoughtWorks
又是一个春暖花开的季节,我离开了东软,即将成为一名ThoughtWorker。
我在东软工作了五年,不算短的时间。其实,我也想过一直留在在东软,因为这里有我成长的足迹,有许多美好的回忆,但几年的工作经验告诉我,这里无法给我我所需要的东西。我早就知道自己会离开,甚至是在进入东软之前,因为这里是“人才的摇篮”。之所以在东软一待就是五年,因为我一直不知道自己要到怎样的地方去做什么。我只知道,我需要一个能够让自己快乐的地方,去做一件让自己兴奋的事情。
如今,之所以选择离开,因为我认为我终于有机会得到了我想要的:一个能够让自己快乐的地方——ThoughtWorks,一件让自己兴奋的事情——Ruby。
很早就知道ThoughtWorks,因为《重构》的作者Martin Fowler是它的首席科学家,因为我有很多朋友在里面。但对ThoughtWorks真正的了解却很少。这次找工作让我有机会相对深入的了解了ThoughtWorks,尤其是面试过程中有一项是到西安的office去面试。
ThoughtWorks的工作环境很特别,给我留下最深印象的就是,除了少数几个做行政的人外,其他人一律围坐在圆桌旁,没有那种常见的格子,所谓的领导也不例外,不像其它的公司,领导要有专门的房间。塞满食品的冰箱并不让我觉得特别,办公司的游戏机也是有所耳闻的,我倒是对一副太极拳的挂图产生了浓厚的兴趣,到处打听谁是练家子。
前前后后接触了好些ThoughtWorker,他们给我留下了非常好的印象。ThoughtWorker们谈到自己的公司,都会有一种油然而生的自豪感,在这个愤青横行的年代是多么不容易啊!ThoughtWorker们都非常能说,或许这和公司的咨询背景不无关系,随便拉来一个都可以说上半天。ThoughtWorker们给人的感觉亲切,这让我整个的应聘过程中都感觉非常放松。
随着参与对XRuby参与的深入,我越来越喜欢Ruby。虽然在国内,大多数人对Ruby只是持观望的态度,但国外Ruby已经越来越红火,ThoughtWorks本身已经有不少的项目在用Ruby做。ThoughtWorks很看好Ruby的未来,希望它在软件开发的世界中,扮演更加重要的角色,除了目前的Web应用之外,也可以进军到企业级开发之中。在这种背景下,JRuby的开发者Ola Bini也加入了ThoughtWorks。当然,ThoughtWorks内部有不少人也很看好XRuby,这也就是说,进入ThoughtWorks之后,我也有机会继续在XRuby上的工作。能在Ruby的尚处于上升期加入到这个过程中,而且有机会与一些世界级的程序员合作,想起来就是一件让人兴奋的事情。
我期待着在ThoughtWorks开始我的新工作!







