深思:Ruby中为什么不能显式的调用私有方法

引子:

今天一位群友问了一个问题:

class C
  def public_method
    self.private_method
  end

  private
  def private_method; end
end

C.new.private_method #=> NoMethodError: private method 'private_method' called (...)

为什么都是在同一个类里的两个方法,又不是类外部,怎么还不能用self调用了呢?

其实他看的书里已经给出了答案: 「因为私有方法只能用隐式的接收者」。

这实际上是Ruby语言的天生的固有的规则,就好像是「地球是圆的」一样。

但实际上他无法理解这种规定,那么强行接受这种规则可好?有人说了,这就是Ruby语言创造者的喜好嘛,他喜欢这样设计,你接受就好了嘛,问什么问。我并不赞同这样的观点。任何事物都不是凭空产生的,这样的设计背后一定是有原因的。

从设计哲学入手

这个世界分成神造和人造。假如你想了解宇宙的结构,为什么地球是圆的,为什么地球要绕太阳旋转,你只需要找造物主问一问,“你是怎么想的?宇宙为什么要设计成这样?”,那么困扰人类的谜题就很容易解答了。但实际上,你找不到这样一位造物主,更别说去问这个问题了。但是,作为计算机语言,作为一个人造的东西,我们只需要找语言作者去问问就能知道他如何设计这门语言,幸好,松本行弘很勤快,写了一本《松本行弘的程序世界》一书让我们来观摩他的思想。

书里开篇就写了他设计Ruby的过程,他首先是把Ruby定位于一门面向对象语言,他确定了三个设计哲学,第一设计哲学是简洁,他把Ruby的面向对象实现变的简洁无比,简洁之中又保持的高度一致性。这里就不细说了,细说可以写一本书,事实上这样的书很多了,Avdi和Sandi就写了好几本,还有Martin Fowler。

面向对象的特性之一是封装(说到这里,今天有人说,封装只是Java和C++的特性,我无言以对)。封装,是为了隐藏信息。为什么隐藏信息?看看我从维基百科复制的例子:

/* 一个面向过程的程序会这样写: */
定义莱丝
莱丝.设置音调(5)
莱丝.吸气()
莱丝.吐气()

/* 而当女人高歌被封装到类中,任何人都可以简单地使用: */
定义莱丝是女人
莱丝.高歌()

看看,面向对象思想充满了人文气息,这很难理解吗?「高歌()」是被暴露出来的,和其他对象通信的,「高歌()」内部,你可以把吸气、吐气、设置音调等方法隐藏起来,其他对象根本不关心这些。所以,我们只需要听女人唱歌就好了,关注她什么时候吸气、什么时候吐气干嘛呢?

这就叫封装。是一种代码组织方式,它实际上是对问题领域进行了良好的界限划分,从而实现了松耦合。否则的话,拿上面的例子来说,女人要「高歌()」,还得让你帮她「吸气()」,是不是有点扯淡?「吸气」「吐气」「调音」都是她自己的事。

如何实现封装?private/public/protected

很多面向对象语言都使用private/public/protected来实现封装。还是刚才那人说「那xxx语言也没有private呀,所以你就当private不存在就好了」,这不是扯淡吗?现在如日中天的Golang语言(它也有面向对象特性),有private关键字吗?那么Golang就不能封装了吗?

话说回来,Ruby中的private其实不是关键字,是个方法。使用方式有两种:

class Girl
  attr_accessor :name
  def initialize(name)
    @name = name
  end

  def sing
    inspiration
    expiration
    train_voice
  end

  private
  def inspiration; end
  def expiration; end
  def train_voice; end
end

girl = Girl.new('你女朋友的名字') #没有女朋友的随便写吧
girl.sing

上面是称为scope的方式,也就是private方法,把class定义的scope从中隔开了。另外的方式是这样的:

class Girl
  attr_accessor :name
  def initialize(name)
    @name = name
  end

  def sing
    inspiration
    expiration
    train_voice
  end

  def inspiration; end
  def expiration; end
  def train_voice; end

  private :inspiration, :expiration, :train_voice
end

girl = Girl.new('你女朋友的名字') #没有女朋友的随便写吧
girl.sing

这是通过方法参数的形式来定义private方法。不管是scope还是方法参数,效果是一样的。

但是调用的时候,你必须使用隐式的接收者,而不能显式的指定,包括指定self,不服气你自己试试。

问题来了:为什么只能隐式调用?

在从底层的Ruby实现解释其原因之前,我们不妨从另一个角度问一下自己:

请问,你自言自语的时候会喊你自己的名字吗?(当然,不排除有喊自己名字的,大千世界,什么奇葩都有)

你在问自己这个问题之前,有没有喊你自己的名字?

从底层源码实现角度来观察为什么private方法只能是隐式接收者

好了,铺垫完了,现在我们从底层Ruby源码实现的角度来解释一下:

我们回顾一下Ruby的对象模型:object.message: 「.」前面的object称为「消息接收者」,「.」后面的message称为「消息」。比如:

贾那啥.("你妈叫你吃饭了")

在Ruby中这些message都被称为「方法」,但是在Ruby底层,是有区分的:

方法:显式的指定了「消息接收者」的函数调用。
函数: 没有显式指定「消息接收者」的函数调用。

有人问了,看把你牛逼的,你咋知道的?呵呵,我翻译的《Ruby Under a Microscope》马上要上市了,中文名是《Ruby源码剖析》,大家记得买,看完你也知道了。

我们也来看看代码:

code =<<END
  puts 1
END

puts RubyVM::InstructionSequence.compile(code).disasm

输出

== disasm: #<ISeq:<compiled>@<compiled>>================================
0000 trace            1                                               (   1)
0002 putself          
0003 putobject_OP_INT2FIX_O_1_C_
0004 opt_send_without_block <callinfo!mid:puts, argc:1, FCALL|ARGS_SIMPLE>, <callcache>
0007 leave  

从YARV指令里看到puts调用没有给定显式的接收者,那么它的函数调用标识是FCALL。

看另外一段代码:

code =<<END
  a = A.new
  a.a
END
puts RubyVM::InstructionSequence.compile(code).disasm

输出

== disasm: #<ISeq:<compiled>@<compiled>>================================
local table (size: 2, argc: 0 [opts: 0, rest: -1, post: 0, block: -1, kw: -1@-1, kwrest: -1])
[ 2] a          
0000 trace            1                                               (   1)
0002 getinlinecache   9, <is:0>
0005 getconstant      :A
0007 setinlinecache   <is:0>
0009 opt_send_without_block <callinfo!mid:new, argc:0, ARGS_SIMPLE>, <callcache>
0012 setlocal_OP__WC__0 2
0014 trace            1                                               (   2)
0016 getlocal_OP__WC__0 2
0018 opt_send_without_block <callinfo!mid:a, argc:0, ARGS_SIMPLE>, <callcache>
0021 leave  

看到了吗,如果给定了显式的接收者,方法调用的时候没有FCALL标识。

好,接下来,我们知道private是一个方法,而不是关键字,那可以看看private的方法定义:

static VALUE
rb_mod_private(int argc, VALUE *argv, VALUE module)
{
    return set_visibility(argc, argv, module, METHOD_VISI_PRIVATE);
}

全局搜索Ruby源码,发现了set_visibility的定义:

static VALUE
set_visibility(int argc, const VALUE *argv, VALUE module, rb_method_visibility_t visi)
{
    if (argc == 0) {
       rb_scope_visibility_set(visi);
    }
    else {
       set_method_visibility(module, argc, argv, visi);
    }
    return module;
}

看得出来,如果private没有给定参数,就设置作用域的可见性,否则就只是对给定参数操作。没有给定参数的情况下,那就是private方法下面所覆盖的作用域。

那么rb_mod_private函数中这个 METHOD_VISI_PRIVATE常量是什么呢?不急,我们慢慢找。先来看看来看看rb_scope_visibility_set的实现:

void
rb_scope_visibility_set(rb_method_visibility_t visi)
{
    vm_cref_set_visibility(visi, FALSE);
}

cref是什么? cref是跟踪词法作用域和类的继承关系的。

又有人问了,看把你牛逼的,你咋知道的?呵呵,我翻译的《Ruby Under a Microscope》马上要上市了,中文名是《Ruby源码剖析》,大家记得买,看完你也知道了。

来看看vm_cref_set_visibility的代码:

static void
vm_cref_set_visibility(rb_method_visibility_t method_visi, int module_func)
{
    rb_scope_visibility_t *scope_visi = (rb_scope_visibility_t *)&rb_vm_cref()->scope_visi;
    scope_visi->method_visi = method_visi;
    scope_visi->module_func = module_func;
}

看到这里,咱们得停下来消化一下: Visibility,这个单词有文化的人都知道,是「可见性」的意思。这里不得不说,Ruby的源码写的也是蛮不错的,你看这命名,包含了十足的语义启示,所以我看明白了,这是设置作用域的可见性

好了,接着往下走吧。那么rb_scope_visibility_t是个什么呢?它是一个结构体:

typedef struct rb_scope_visi_struct {
     rb_method_visibility_t method_visi : 3;
     unsigned int module_func : 1;
} rb_scope_visibility_t;

这里有奇怪的数字,我们再顺藤摸瓜,找找 rb_method_visibility_t的定义,结果发现:

/* cref */

typedef enum {
    METHOD_VISI_UNDEF     = 0x00,
    METHOD_VISI_PUBLIC    = 0x01,
    METHOD_VISI_PRIVATE   = 0x02,
    METHOD_VISI_PROTECTED = 0x03,

    METHOD_VISI_MASK = 0x03
} rb_method_visibility_t;
原来Ruby内部是用16进制数字来表示作用域的可见性,
  • undef的方法可见性是0
  • public方法可见性是1
  • private方法可见性是2
  • protected可见性是3.


那么我就明白了,这个rb_scope_visi_struct结构体默认是3. 是protected。我们总算知道前面的METHOD_VISI_PRIVATE是啥意思了。

METHOD_VISI_MASK看字母意思,可能是用来做位掩码,在Ruby源码里全局搜索以后,发现确实如此:
rb_print_inaccessible(VALUE klass, ID id, rb_method_visibility_t visi)
{
    const int is_mod = RB_TYPE_P(klass, T_MODULE);
    VALUE mesg;
    switch (visi & METHOD_VISI_MASK) {
      case METHOD_VISI_UNDEF:
      case METHOD_VISI_PUBLIC:    mesg = inaccessible_mesg(""); break;
      case METHOD_VISI_PRIVATE:   mesg = inaccessible_mesg(" private"); break;
      case METHOD_VISI_PROTECTED: mesg = inaccessible_mesg(" protected"); break;
      default: UNREACHABLE;
    }
    rb_name_err_raise_str(mesg, klass, ID2SYM(id));
}
继续往下看,rb_vm_cref()是啥呢?来看看
rb_cref_t *
rb_vm_cref(void)
{
    rb_thread_t *th = GET_THREAD();
    rb_control_frame_t *cfp = rb_vm_get_ruby_level_next_cfp(th, th->cfp);

    if (cfp == NULL) {
     return NULL;
    }

    return rb_vm_get_cref(cfp->ep);
}

这里返回的是rb_vm_get_cref(cfp->ep);的求值:

static rb_cref_t *
rb_vm_get_cref(const VALUE *ep)
{
    rb_cref_t *cref = vm_env_cref(ep);

    if (cref != NULL) {
     return cref;
    }
    else {
     rb_bug("rb_vm_get_cref: unreachable");
    }
}

ep是环境指针,ep是记录上下文帮助实现闭包的。

有人问了,看把你牛逼的,你咋知道的?呵呵,我翻译的《Ruby Under a Microscope》马上要上市了,中文名是《Ruby源码剖析》,大家记得买,看完你也知道了。

这里我们分析private就不用管这个了,所以知道它只是返回已记录的cref。

而rb_cref_t 是一个结构体:

typedef struct rb_cref_struct {
    VALUE flags;
    const VALUE refinements;
    const VALUE klass;
    struct rb_cref_struct * const next;
    const rb_scope_visibility_t scope_visi;
} rb_cref_t;

该结构体跟踪了代码所属的词法作用域(next指针),当然还有其他信息,比如类的继承关系(klass指针) 。

有人问了,看把你牛逼的,你咋知道的?呵呵,我翻译的《Ruby Under a Microscope》马上要上市了,中文名是《Ruby源码剖析》,大家记得买,看完你也知道了。

然而书里没说的是,这个结构体其实也记录了词法作用域的可见性,这里定义了一个常量:scope_visi

然后我们回到 rb_mod_private函数中:

return set_visibility(argc, argv, module, METHOD_VISI_PRIVATE);

所以,上面vm_cref_set_visibility函数中rb_vm_cref()->scope_visi;的意思是:开一个新的栈帧返回一个rb_cref_t结构体,那么这个时候scope_visi 的值是3,那么其实到了这里,这样一层一层传下来,scope_visi的值就是2了。

再看vm_cref_set_visibility函数:


rb_scope_visibility_t *scope_visi = (rb_scope_visibility_t *)&rb_vm_cref()->scope_visi;
scope_visi->method_visi = method_visi;
scope_visi->module_func = module_func;

vm_cref_set_visibility函数里重新定义了一个scope_visi结构体,新增了两个属性method_visi和module_func,这俩属性是当private方法里传入参数的时候才有用,比如:

class A
  def a
    puts 1
  end
  private :a
end

就这样,private方法定义了该下方作用域的可见性,当Ruby在函数调用的时候:

vm_call_method(rb_thread_t *th, rb_control_frame_t *cfp, struct rb_calling_info *calling, const struct rb_call_info *ci, struct rb_call_cache *cc)
{
    VM_ASSERT(callable_method_entry_p(cc->me));

    if (cc->me != NULL) {
     switch (METHOD_ENTRY_VISI(cc->me)) {
       case METHOD_VISI_PUBLIC: /* likely */
         return vm_call_method_each_type(th, cfp, calling, ci, cc);

       case METHOD_VISI_PRIVATE:
         if (!(ci->flag & VM_CALL_FCALL)) {
          enum method_missing_reason stat = MISSING_PRIVATE;
          if (ci->flag & VM_CALL_VCALL) stat |= MISSING_VCALL;

          cc->aux.method_missing_reason = stat;
          CI_SET_FASTPATH(cc, vm_call_method_missing, 1);
          return vm_call_method_missing(th, cfp, calling, ci, cc);
         }
         return vm_call_method_each_type(th, cfp, calling, ci, cc);
...
}

想看完整函数定义的可以点击该代码源码位置

当case是PRIVATE的分支的时候,会判断是否是FCALL或VCALL,这意味着什么呢? 如果不是FCALL或VCALL,则抛出方法找不到的异常错误,也就是开篇看到的示例中的错误。

至此,我们已经相对完整的给出了文章标题所示问题的答案。

其他收获

vm_cref_new_toplevel(rb_thread_t *th)
     {
         rb_cref_t *cref = vm_cref_new(rb_cObject, METHOD_VISI_PRIVATE /* toplevel visibility is private */, FALSE, NULL, FALSE);
     
         cref = vm_cref_new(th->top_wrapper, METHOD_VISI_PRIVATE, FALSE, cref, FALSE);
        }
    
         return cref;
     }
在搜索中还发现了Ruby默认顶级作用域是private的事实。

小结

读者自己总结吧。

看完别忘记打个赏。

呵呵,我翻译的《Ruby Under a Microscope》马上要上市了,中文名是《Ruby源码剖析》,大家记得买。

更新: 《Ruby源码剖析》现已上市,购买链接

当当预售地址

京东购买地址

编辑于 2016-12-03

文章被以下专栏收录