作為一個Ruby開發(fā)者,讓人又愛又恨的便是元編程了。
【前言】元編程是什么
簡單地說,元編程就是對語言本身的進行操作的一種編程手段,最常見的就是代碼生成代碼。對于Ruby這門語言而言,不會元編程,等于不會這門語言,因為這是它的核心能力與魅力。本文是基于閱讀《Ruby元編程》后記錄的一些自己的理解和看法。
元編程示例【示例1】
module Kernel
def attr_access(*args)
args.each do |arg|
define_method(arg) do
instance_variable_get("@#{arg}")
end
define_method("#{arg}=") do |val|
instance_variable_set("@#{arg}", val)
end
end
end
end
class Student
attr_access :name, :age
end
stu = Student.new
stu.age = 20
stu.name = 'Rapheal'
p stu.inspect
【示例1】一個典型元編程的例子,它實現(xiàn)了Ruby中自帶的attr_accessor相同的功能,作用是動態(tài)的為傳入的參數(shù)(上面代碼中是:name和:age)添加setter和getter方法(stu.age=xxx為其setter方法, stu.age為其getter方法)。這樣的方法避免了類似Java中的長篇setter和getter定義。
【主題】對象模型
Ruby作為一種完全面向?qū)ο蟮木幊陶Z言,即使是一個數(shù)字、類、甚至一個方法都是一個對象。所謂對象,就是能對它進行一系列操作的一個集合。
打開類
對象模型篇第一講就是打開類。在【示例1】代碼中其實就已經(jīng)包含了打開類的一種具體實現(xiàn)方法。打開類,即打開一個已經(jīng)存在的類或?qū)ο?,為?code>添加新方法、修改已存在的方法或刪除不需要的方法的一種技術。在【示例1】中,Kernel是Ruby庫中已經(jīng)存在的一個模塊,使用module Kernel將其重新打開,并添加了一個新方法attr_access。于是Kernel模塊便在原來的基礎上新增了一個方法attr_access。
修改一個已經(jīng)存在的方法【示例2】
str = "abc"
p str.to_s # 這里會輸出"abc"
class String
def to_s
"Nothing"
end
end
str = "abc"
p str.to_s # 這里輸出的就是"Nothing"了
String也是Ruby庫自帶的類,to_s是String類已存在的方法,當重新打開它并重寫了to_s方法之后,原來方法的作用便不復存在了,取而代之的是新方法的作用。(這種修改已經(jīng)存在的方法又被稱為猴子補丁)
打開類的利與弊
通過【示例1】與【示例2】的代碼可以知道,打開類技術可以很好的對已經(jīng)存在的類或方法進行修改,使之更符合個人的使用需求。然而,若不加以思考隨意使用,帶來的問題也是很嚴重的。比如String類的to_s方法,作用就是要返回本身這個字符串,結果被別人修改了這個定義,導致了所有引用這個方法的代碼全部失去了它本來的功能與意義。因此在使用打開類定義一個方法時,需要謹慎,盡量取一個當前不存在的方法名來新定義一個方法。
對象中有什么
首先,實例變量,如【示例1】中的:name和:age,當調(diào)用stu.name = 'rapheal'之后,stu對象便產(chǎn)生了一個實例變量@name。實例變量必須是以【一個@符號】開頭的變量名。這時可以通過調(diào)用stu.instance_variables來查看已經(jīng)存在的實例變量,可以看到輸出中有:@name這一條。
其次,方法。通過stu.methods可以查看stu對象能調(diào)用的所有方法。Ruby對象共享方法,但不共享實例變量,共享的方法被稱為【實例方法】。【實例方法】定義在對象的類中,這樣可以使得同一類對象可以調(diào)用相同的方法。
類也是對象。類對象所屬的類是Class類。類的方法即為Class類中定義的【實例方法】。比如,所有類都有一個方法new,而new方法的定義就在Class類中。我們甚至可以簡單的認為:ClassA = Class.new和class ClassA; end是等價的。它們都是在定義一個新的類ClassA。
方法查找
提到方法查找,那么首先要知道的就是祖先鏈。祖先鏈其實就是記錄的一個類的繼承關系的一個列表,可以通過調(diào)用ancestors方法來查看。比如String.ancestors返回的是[String, Comparable, Object, Kernel, BasicObject],于是我們可以判斷,String類繼承自Object,(Comparable和Kernel是兩個module,它被包含在了其中的某個類中,也會出現(xiàn)在祖先鏈中來,此處我們不討論祖先鏈中的module),Object又繼承自BasicObject。
理解了方法鏈,再回頭來看方法查找。Ruby中的方法查找有個原則叫作向右,再向上。比如,有一個String類的對象str,調(diào)用方法str.test_call_method,這時Ruby解釋器會:
- 1、
【向右】來到str所屬的String類查看String類是否定義了test_call_method這個方法,若定義了則直接調(diào)用 - 2、
【向上】否則查看Comparable這個module中是否定義這個方法(因為祖先鏈中有這個module,并且排在了第二個,即String類和Object類中間) - 3、
【向上】若還未定義,則來到父類Object類查找 - 4、重復上述2、3步驟直到
BasicObject類
上述步驟中,步驟1稱為向右,步驟2、3稱為向上。整個流程中,可以看出,方法查找是優(yōu)先向右(所屬類)查找,再向上(優(yōu)先是自身包含的模塊然后是父類)查找。因此稱為向右,再向上原則。
對于類所包含的模塊會在方法查找時定義為一個匿名類并插入到祖先鏈中該類的直接上方。
關于self
在某個特定時刻,一定會有一個指定的對象在執(zhí)行,這個對象就是self對象。最開始接觸這個的時候,會有一個誤區(qū)認為self是當前調(diào)用方法的執(zhí)行者,然而事實上self是當前方法執(zhí)行的接收者。簡單說即是,當前方法調(diào)用的結果會傳遞返回給這個self對象。
談到self,那么就應該順便說一下private。Ruby中的private是和self相關的,在Ruby類的定義的private方法是不能被顯式調(diào)用的。
private示例【示例3】
class A
def print_self
self.t_pri
end
def print_self_2
t_pri
end
private
def t_pri
p "hello world"
end
end
obj1 = A.new
obj1.print_self_2 # 輸出 "hello world"
obj1.print_self # 報錯, NoMethodError: private method 't_pri' called
obj1.t_pri # 報錯,同上
從【示例3】,可以看出私有方法t_pri只能由self隱式調(diào)用,即私有方法只能在定義的內(nèi)部以直接調(diào)用方式調(diào)用,而不能在任何地方以 xxx.yyyy的方式調(diào)用。同時,若沒有顯式指定方法接收者,那么調(diào)用方法的接收都將隱式指定為self對象。