• 2006-09-18

    管窥Ruby——类的方法

    版权声明:转载时请以超链接形式标明文章原始出处和作者信息及本声明
    http://www.blogbus.com/dreamhead-logs/3350630.html

    在大学里最初接触C++的时候,一个比我高一年级的师兄告诉我,C++就是结构里加入了函数,这便是我对面向对象的最初的认识。那时的我,没有太多的经验,也不知道什么原则。这样一个最简单的印象一直持续到今天,当然,今天谈起面向对象,我可以说很多,但由繁化简,最基础的东西是数据和方法。

    在《管窥Ruby——对象基础》中,我们看到了类的结构:RClass。为了加深印象,先来回顾一下RClass的定义:
    struct RClass {
        struct RBasic basic;
        struct st_table *iv_tbl;
        struct st_table *m_tbl;
        VALUE super;
    };
    (ruby.h)

    先来看看,iv_table和m_tbl的类型:st_table,其实它就是一个hash表,当然,名字让人有些混淆。它声明在st.h中,实现在st.c中。这个hash表是学习数据结构的一个好例子,它的实现很清楚。不过,这段代码并非Ruby的产品,而是从他处借鉴而来,文件开头的声明,说明了这一点:
    /* This is a public domain general purpose hash table package written by Peter Moore @ UCB. */

    这次管窥的目标是类的方法。

    知道了方法存储的结构,它的使用方法便也不难估计。要执行一个方法,大约就是一个查表的过程,下面的代码证实了这一点:
    static NODE*
    search_method(klass, id, origin)
        VALUE klass, *origin;
        ID id;
    {
        NODE *body;

        if (!klass) return 0;
        while (!st_lookup(RCLASS(klass)->m_tbl, id, (st_data_t *)&body)) {
            klass = RCLASS(klass)->super;
            if (!klass) return 0;
        }

        if (origin) *origin = klass;
        return body;
    }
    (eval.c)

    这段代码相当于在klass中查找名为id的方法。VALUE的用法在《管窥Ruby——对象基础》中已经提到过。需要解释一下的是id,它的类型是ID,ID定义如下:
    typedef unsigned long ID;
    (ruby.h)

    按照我们通常的理解,函数应该以其名字进行表示,这并不矛盾,因为在Ruby实现中,字符串和ID之间有个一一对应的关系,用ID而非原始的字符串,在处理的性能上要得到一些提升,ID和原始字符串之间的转换实际上也是有st_table来完成的。

    在这段代码上,我们可以看到了查找方法的方式,如果在该类的方法中找不到,就会沿着超类向上找。

    熟悉Java或C++的人可能看出了一些差异,这里没有传说中的虚拟函数表。回忆一下,使用虚拟函数表的调用方式,其查找过程就是以在虚拟函数表中的偏移(相当于数组索引)找到这个函数,通常,这种方法要比这里查表的方法快一些。

    牺牲性能,换来的是灵活。使用查表法,我们用的是名字与方法体对应,如果需要,我们在运行时可以在这个表中添加新的项,这也就实现了所谓的动态,也成为许多人吹捧动态语言的一个重要基础。有了这样的基础,我们不难理解那些看上去似乎神奇的做法,比如动态修改代码,比如为已有的类添加新的方法,要做的只是修改对应函数名的实现或是添加一个新的表项而已。当然,要做到动态,不只这一点这么简单,还要有其它的部分与之配合。

    使用虚拟函数表的方法,表项在编译时便已固定,把函数映射为在虚拟函数表中的偏移,到了运行时,只知道“偏移、执行”,至于究竟是哪个函数,无从知晓。类似于查表的过程,在编译的时候一定是存在的,但不存在于运行时。对程序而言,从源码到运行是一个完整的过程,一些功能被某些语言放到了编译时,而在另一些语言中被放到了运行时,折衷的原则取决于语言设计。

    两种方法还有一个差别。我们已经看到了查表的实现,每个类只要保持它自己的方法就可以,但依然可以使用超类的方法,去它的超类中找就是了。虚拟函数表的实现中,每个类的表中,不仅仅要保持自己的定义的方法,还要保持自己超类的方法,我们知道,在面向对象的语言中,子类对象常常要当作超类对象使用,而在运行时,要找某个方法,只知“偏移”,所以,子类的虚拟函数表必须完全兼容超类的虚拟函数表,才能保证整个系统的正常运行,而保证的方法就是保存超类的所有表项。

    这样带来的问题是,当子类增多,虚拟函数表就无可避免的会增多,即便子类只有一个属于自己的方法,但它仍要带有超类所有的方法,这是一个巨大的负担。所以,那些建议“不要定义太庞杂的继承系统”的说法,是有一定物理基础的。

    当然,我们可以想,是否虚拟表可以像动态语言这样,每个类只保持自己的方法,找不到的话,逆流向上。不要忘了,虚拟表本身是为了让运行更快,这样的实现会让性能受到损失,那样做便背离了语言设计的初衷。而且前面已经说过了,方法的信息在编译时已经消除了,若要判断找的方法是不是自己要找的方法,岂不是要增加额外的信息。

    查表不是一个快速的选择,在具体的实现中,Ruby中有一个缓存的实现,将一些调用过的方法缓存起来,调用方法的时候会先看方法是否已存在于缓存之中,这样的话,可以提升一些函数调用的性能。

    分享到:

    历史上的今天:

    Hello,Vagrant 2012-09-18
    做好一件事 2009-09-18
    引用地址: