• 2007-05-14

    添加方法的小魔术

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

    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只是一个简单标识,这是编程中很常用的一种方法,表示一件事情的完成,没有什么难度。只是理解添加方法这个问题时,可能会一不小心走到误区里面,正如我前面提到的那个有些绕的说法。
    分享到:

    历史上的今天:

    引用地址:

    评论

  • 关于clear_cache,我怀疑加上那样的判断只是为了防止冲突,毕竟初始化的时候,应该是没有方法cache的.