Ruby Ruby 内联私有方法与原理

dsh0416 · 2018年07月03日 · 最后由 mz2test 回复于 2018年07月25日 · 2855 次阅读
本帖已被设为精华帖!

孔乙己有一回对我说道,“你学过 Ruby 么?”我略略点一点头。他说,“学过 Ruby,……我便考你一考。private 私有方法,怎样写的?”我想,讨饭一样的人,也配考我么?便回过脸去,不再理会。孔乙己等了许久,很恳切的说道,“不能写罢?……我教给你,记着!这些字应该记着。将来做 CTO 的时候,写码要用。”我暗想我和 CTO 的等级还很远呢,而且我们 CTO 也从不在 Ruby 里写 private 方法;又好笑,又不耐烦,懒懒的答他道,“谁要你教,不是 private 换行之后就是 def 的都是私有方法么?”孔乙己显出极高兴的样子,将两个指头的长指甲敲着柜台,点头说,“对呀对呀!…… private 的三种写法,你知道么?”我愈不耐烦了,努着嘴走远。孔乙己刚用指甲蘸了酒,想在柜上写字,见我毫不热心,便又叹一口气,显出极惋惜的样子。

孔乙己这里说的 private 的三种写法其中有两种是非常常见的,第一种是:

class Test
    private
    def a
        'a'
    end
end

另一种则是:

class Test
    def a
        'a'
    end
    private :a
end

这两种各有优劣,第一种 private 后的方法都是私有方法,有效组织了方法顺序。而第二种则可以随意定义方法,之后再定义哪些是 private 方法。

之前在看 Rubocop 的代码风格的一些 Issue 讨论的时候,看到了 private 的第三种写法,目前 midori 项目中使用的是这种作法:内联私有方法。

class Test
    private def a
        'a'
    end
end

这种写法的好处是,可以很清楚知道某一个方法是不是私有方法,也可以随意组织方法的顺序。但是奇怪了,似乎很少有看到 Ruby 正式的文档里提到过这种写法,这到底是一种什么写法?Ruby 为什么支持这种写法?

我们运行一下:

ruby -e "class Test; private def a; 'a'; end; end" --dump parseTree

看一下 Ruby 对这段魔法代码的 AST 树:

###########################################################
## Do NOT use this node dump for any purpose other than  ##
## debug and research.  Compatibility is not guaranteed. ##
###########################################################

# @ NODE_SCOPE (line: 1, code_range: (1,0)-(1,40))
# +- nd_tbl: (empty)
# +- nd_args:
# |   (null node)
# +- nd_body:
#     @ NODE_PRELUDE (line: 1, code_range: (1,0)-(1,40))
#     +- nd_head:
#     |   (null node)
#     +- nd_body:
#     |   @ NODE_CLASS (line: 1, code_range: (1,0)-(1,40))
#     |   +- nd_cpath:
#     |   |   @ NODE_COLON2 (line: 1, code_range: (1,6)-(1,10))
#     |   |   +- nd_mid: :Test
#     |   |   +- nd_head:
#     |   |       (null node)
#     |   +- nd_super:
#     |   |   (null node)
#     |   +- nd_body:
#     |       @ NODE_SCOPE (line: 1, code_range: (1,0)-(1,40))
#     |       +- nd_tbl: (empty)
#     |       +- nd_args:
#     |       |   (null node)
#     |       +- nd_body:
#     |           @ NODE_BLOCK (line: 1, code_range: (1,10)-(1,35))
#     |           +- nd_head (1):
#     |           |   @ NODE_BEGIN (line: 1, code_range: (1,10)-(1,10))
#     |           |   +- nd_body:
#     |           |       (null node)
#     |           +- nd_head (2):
#     |               @ NODE_FCALL (line: 1, code_range: (1,12)-(1,35))
#     |               +- nd_mid: :private
#     |               +- nd_args:
#     |                   @ NODE_ARRAY (line: 1, code_range: (1,20)-(1,35))
#     |                   +- nd_alen: 1
#     |                   +- nd_head:
#     |                   |   @ NODE_DEFN (line: 1, code_range: (1,20)-(1,35))
#     |                   |   +- nd_mid: :a
#     |                   |   +- nd_defn:
#     |                   |       @ NODE_SCOPE (line: 1, code_range: (1,20)-(1,35))
#     |                   |       +- nd_tbl: (empty)
#     |                   |       +- nd_args:
#     |                   |       |   @ NODE_ARGS (line: 1, code_range: (1,25)-(1,25))
#     |                   |       |   +- nd_ainfo->pre_args_num: 0
#     |                   |       |   +- nd_ainfo->pre_init:
#     |                   |       |   |   (null node)
#     |                   |       |   +- nd_ainfo->post_args_num: 0
#     |                   |       |   +- nd_ainfo->post_init:
#     |                   |       |   |   (null node)
#     |                   |       |   +- nd_ainfo->first_post_arg: (null)
#     |                   |       |   +- nd_ainfo->rest_arg: (null)
#     |                   |       |   +- nd_ainfo->block_arg: (null)
#     |                   |       |   +- nd_ainfo->opt_args:
#     |                   |       |   |   (null node)
#     |                   |       |   +- nd_ainfo->kw_args:
#     |                   |       |   |   (null node)
#     |                   |       |   +- nd_ainfo->kw_rest_arg:
#     |                   |       |       (null node)
#     |                   |       +- nd_body:
#     |                   |           @ NODE_STR (line: 1, code_range: (1,27)-(1,30))
#     |                   |           +- nd_lit: "a"
#     |                   +- nd_next:
#     |                       (null node)
#     +- nd_compile_option:
#         +- coverage_enabled: false

关键地方在

...
#     |           +- nd_head (2):
#     |               @ NODE_FCALL (line: 1, code_range: (1,12)-(1,35))
#     |               +- nd_mid: :private
#     |               +- nd_args:
#     |                   @ NODE_ARRAY (line: 1, code_range: (1,20)-(1,35))
#     |                   +- nd_alen: 1
#     |                   +- nd_head:
#     |                   |   @ NODE_DEFN (line: 1, code_range: (1,20)-(1,35))
#     |                   |   +- nd_mid: :a
#     |                   |   +- nd_defn:
...

可见,这里的 private 被视作一个方法,而接收的参数的 def 定义的方法的返回。def 的返回是什么呢?是方法的 Symbol。也就是说:

private def a
    'a'
end

# 等价于

tmp = def a
    'a'
end

tmp # => :a

private tmp

可见这个方法并不是真正所谓的内联,并没有什么 private def 的关键字,只是使用了一些 tricky 的方法使它看起来是内联的。那么这种写法有什么副作用?

让我们梦回一下 2013 年,看一眼 2013 年 Ruby 2.1 的发布新闻

def-expr now returns the symbol of its name instead of nil.

也就是说 def 表达式返回其名字的 Symbol 正是当时引入的特性。在这个特性的支持下,也就可以实现内联私有方法这种 trick 了。如果你的项目仍需要向下兼容到 Ruby 2.1 就不能使用这种方法了。

考虑到 Ruby 2.2 也已经 EOL 了,是否是用这种方法来实现私有方法已经完全变成了一种代码风格的取舍。如果你喜欢这种风格的私有方法,不妨在下一个项目里试一试吧。

共收到 11 条回复

沙发

涨姿势

之前这样用过,不过真没想到背后的原理是这样

顺便学走一招 --dump parseTree 👏

还有这种骚操作,涨姿势了。不过大部分情况下,第一种已经足够了。就算是写java、C#,我也是习惯把私有方法归类都写在底部。

试了下,发现class method也可以这样用

一般写法

def self.func_a
  puts 'func_a'
end

private_class_method :func_a

放函数左边

private_class_method def self.func_a
  puts 'func_a'
end

还是习惯这样写

class Test
  class << self
    private

    def func_a
      puts 'func_a'
    end

    def func_b
      puts 'func_b'
    end
  end
end

instance method

还是喜欢第一种写法,类里面写一个就行了,一般都喜欢放类最后面

class Test
  private

  def func_a
    puts 'func_a'
  end

  def func_b
    puts 'func_b'
  end
end

Koichi Sasada 介绍 2.1 特性时讲过 https://ruby-china.org/topics/14361

用途 private static void def ...

jasl 将本帖设为了精华贴 07月03日 18:00

搭配 --dump parsetree_with_comment 食用更加

luikore 回复

打死java🐶。。

不管哪种写法 四格缩紧是不能忍的

乍一看,最后一种很像java中的修饰符。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册