Ruby 內聯私有方法與原理

Ruby 內聯私有方法與原理

來自專欄 Ruby 魔法的養成方法17 人贊了文章

孔乙己有一回對我說道,「你學過 Ruby 么?」我略略點一點頭。他說,「學過 Ruby,……我便考你一考。private 私有方法,怎樣寫的?」我想,討飯一樣的人,也配考我么?便回過臉去,不再理會。孔乙己等了許久,很懇切的說道,「不能寫罷?……我教給你,記著!這些字應該記著。將來做 CTO 的時候,寫碼要用。」我暗想我和 CTO 的等級還很遠呢,而且我們 CTO 也從不在 Ruby 里寫 private 方法;又好笑,又不耐煩,懶懶的答他道,「誰要你教,不是 private 換行之後就是 def 的都是私有方法么?」孔乙己顯出極高興的樣子,將兩個指頭的長指甲敲著櫃檯,點頭說,「對呀對呀!…… private 的三種寫法,你知道么?」我愈不耐煩了,努著嘴走遠。孔乙己剛用指甲蘸了酒,想在柜上寫字,見我毫不熱心,便又嘆一口氣,顯出極惋惜的樣子。

孔乙己這裡說的 private 的三種寫法其中有兩種是非常常見的,第一種是:

class Test private def a a endend

另一種則是:

class Test def a a end private :aend

這兩種各有優劣,第一種 private 後的方法都是私有方法,有效組織了方法順序。而第二種則可以隨意定義方法,之後再定義哪些是 private 方法。

之前在看 Rubocop 的代碼風格的一些 Issue 討論的時候,看到了 private 的第三種寫法,目前 midori 項目中使用的是這種作法:內聯私有方法。

class Test private def a a endend

這種寫法的好處是,可以很清楚知道某一個方法是不是私有方法,也可以隨意組織方法的順序。但是奇怪了,似乎很少有看到 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 aend?# 等價於?tmp = def a aend?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 了,是否是用這種方法來實現私有方法已經完全變成了一種代碼風格的取捨。如果你喜歡這種風格的私有方法,不妨在下一個項目里試一試吧。

推薦閱讀:

入坑Go語言(一)—— 基礎語法
兒童編程語言MIT App Inventor和Scratch比較那個好?
AI編程:5種最流行的人工智慧編程語言
如何自學編程?
Python中的變數、對象、引用

TAG:Ruby | 編程語言 | 原理 |