Ruby是一門動(dòng)態(tài)語(yǔ)言忘嫉,動(dòng)態(tài)創(chuàng)建與調(diào)用方法是其中一個(gè)體現(xiàn)盖奈。
動(dòng)態(tài)方法
動(dòng)態(tài)調(diào)用方法(動(dòng)態(tài)派發(fā))
動(dòng)態(tài)調(diào)用方法拆又,是指在代碼中不通過(guò)硬編碼而是在程序運(yùn)行時(shí)自動(dòng)去決定要調(diào)用的方法的一種行為养晋。
示例代碼1
class Student
attr_accessor :name, :age, :birthday
def initialize(args = {})
name = args[:name]
age = args[:age]
birthday = args[:birthday]
end
end
【示例代碼1】
中initialize
方法中偏窝,給三個(gè)字段賦值的方式就是一種典型的硬編碼方式收恢,假如這三個(gè)字段的名稱有改動(dòng),抑或添加祭往、去掉字段的時(shí)候伦意,不得不同時(shí)修改這個(gè)方法。為了避免這種情況硼补,這里可以考慮使用動(dòng)態(tài)調(diào)用的方式來(lái)重構(gòu)它驮肉。
示例代碼2
class Student
attr_accessor :name, :age, :birthday
def initialize(args = {})
args.each do |key, value|
method_name = "#{key}="
self.send("#{key}=", value) if self.respond_to?(method_name)
end
end
end
通過(guò)【示例代碼2】
的重構(gòu),但凡attr_accessor
后面的字段有變動(dòng)時(shí)括勺,initialize
方法都會(huì)自動(dòng)進(jìn)行適配缆八。那么實(shí)現(xiàn)的原理是什么呢曲掰?
在Ruby中,方法調(diào)用其實(shí)是向一個(gè)對(duì)象發(fā)送了一條消息奈辰,當(dāng)接收方接收消息后栏妖,會(huì)在對(duì)象的祖先鏈中去尋找這個(gè)方法,找到之后調(diào)用它并返回給self
對(duì)象(詳細(xì)見(jiàn)【對(duì)象模型篇】)奖恰。也就是說(shuō)吊趾,當(dāng)調(diào)用str.method
的時(shí)候,本質(zhì)上就是發(fā)送了一條方法調(diào)用的消息瑟啃,接收者是str
對(duì)象论泛,它等價(jià)于str.send(:method)
。因此示例代碼便很好理解了蛹屿,它是將args
這個(gè)hash中的值進(jìn)行遍歷屁奏,動(dòng)態(tài)調(diào)用attr_accessor
生成的setter
和getter
方法。但有一個(gè)問(wèn)題错负,如果參數(shù)中有在attr_accessor
未定義的字段怎么辦坟瓢?比如Student.new({ year: 2016 })
,year
字段是未在attr_accessor
中定義的犹撒,如果調(diào)用self.year =
這個(gè)方法折联,是會(huì)拋異常的。所以這里添加了respond_to?
來(lái)判斷這個(gè)方法是否是存在的识颊,存在再對(duì)它進(jìn)行調(diào)用賦值诚镰。
動(dòng)態(tài)定義方法
關(guān)于動(dòng)態(tài)定義方法,其實(shí)在第一章對(duì)象模型篇的【示例代碼1】
已經(jīng)在使用了祥款,就是對(duì)define_method
的使用清笨。在此基礎(chǔ)之上,此處實(shí)現(xiàn)一個(gè)更加具有可用性的案例:
示例代碼3
module Kernel
def attr_access(*args)
args.each do |arg|
define_method(arg) do
instance_variable_get("@#{arg}")
end
define_method("#{arg}=") do |value|
instance_variable_set("@{arg}=", value)
end
end
end
def cattr_access(*args)
args.each do |arg|
define_singleton_method(arg) do
self.class_variable_get("@@#{arg}")
end
define_singleton_method("#{arg}=") do |value|
self.class_variable_set("@@#{arg}", value)
end
end
end
end
class A
cattr_access :a
end
A.a = 1
p A.a # 輸出1
此處不再說(shuō)明define_method
和attr_access
的使用镰踏,重點(diǎn)說(shuō)明一下define_singleton_method
和cattr_access
的實(shí)現(xiàn)函筋。
define_singleton_method
和define_method
的區(qū)別是,前者定義的是單例方法
(這里可稱為類方法)奠伪,后者定義的是實(shí)例方法
跌帐。從用法來(lái)看,cattr_access
聲明的變量直接在類(這里是A
)上調(diào)用绊率,而attr_access
聲明的變量需要在A
類對(duì)象實(shí)例化(A.new
)之后調(diào)用谨敛。同理,class_variable_set
和class_variable_get
定義的是單例變量(這里指類變量)滤否,而instance_variable_set
和instance_variable_get
定義的是實(shí)例變量脸狸。由于Ruby的語(yǔ)法約定,以@
開(kāi)頭的為實(shí)例變量,以@@
開(kāi)頭的為類變量炊甲,因此泥彤,在定義變量時(shí)尤其要注意變量的全名,否則會(huì)拋異常卿啡。
幽靈方法
還記得之前在方法查找中吟吝,如果找不到方法時(shí),會(huì)觸發(fā)一個(gè)NoMethodError
的異常拋出颈娜。然而它來(lái)源于向?qū)ο蟀l(fā)送了一個(gè)消息調(diào)用了一個(gè)方法叫做method_missing
剑逃。
假如對(duì)一個(gè)String
類對(duì)象str
調(diào)用test_method_a
,即str.test_method_a
官辽,由于這個(gè)方法未定義蛹磺,因此在祖先鏈中找不到這個(gè)方法。此時(shí)會(huì)發(fā)送一個(gè)消息str.send(:method_missing, :test_method_a)
同仆,從而拋出NoMethodError
的異常萤捆。也就是說(shuō),當(dāng)找不到要調(diào)用的方法時(shí)乓梨,會(huì)自動(dòng)觸發(fā)調(diào)用method_missing
方法鳖轰。那么如果重寫(xiě)了某個(gè)類的method_missing
方法會(huì)是什么樣的結(jié)果呢清酥?
示例代碼4
class XmlGen
def method_missing(name, *args, &block)
if %W(html head title body).include?(name.to_s)
define_singleton_method(name) do |arg = nil, &blk|
str = "<#{name}>"
str += arg if arg
str += blk.call if blk
str += "</#{name}>"
str
end
self.send(name, *args, &block)
end
end
end
xml = XmlGen.new
str = xml.html do
xml.head do
xml.title "Test"
end
end
p str # 輸出<html><head><title>Test</title></head></html>
由于在method_missing
中對(duì)調(diào)用方法的名字做了限制扶镀,必須是html
、head
焰轻、title
臭觉、body
其中之一才會(huì)生成代碼,因此無(wú)需擔(dān)心其它額外正常調(diào)用不存在方法的時(shí)候不能正常拋出NoMethodError
異常的情況辱志。由于在調(diào)用不存在的方法時(shí)就會(huì)調(diào)用method_missing
這個(gè)方法蝠筑,因此如果要重寫(xiě)這個(gè)方法一定要格外小心,能力越大揩懒,責(zé)任越大
什乙。
幽靈方法與普通動(dòng)態(tài)方法的優(yōu)劣
普通動(dòng)態(tài)方法是指,在類初始化時(shí)便使用define_method
等手段將需要的所有方法定義好已球。幽靈方法本質(zhì)是在調(diào)用時(shí)臣镣,如果發(fā)現(xiàn)不存在方法時(shí),那么即時(shí)定義這個(gè)方法并產(chǎn)生一次調(diào)用智亮,從示例可以看出幽靈方法在定義方法時(shí)也是調(diào)用的define_method
等行為來(lái)定義動(dòng)態(tài)方法忆某。與普通定義動(dòng)態(tài)方法的區(qū)別是,如果一個(gè)對(duì)象永遠(yuǎn)沒(méi)有調(diào)用一個(gè)方法阔蛉,那么這個(gè)方法永遠(yuǎn)不會(huì)被定義弃舒,只有調(diào)用過(guò)一次時(shí)它才會(huì)被定義,因此使用幽靈方法時(shí)状原,對(duì)象所占用的內(nèi)存空間比普通動(dòng)態(tài)方法要少聋呢,反之付出的代價(jià)是第一次在祖先鏈中查找該方法的時(shí)間變長(zhǎng)苗踪。這可以認(rèn)為是一種以時(shí)間換取空間的策略。
動(dòng)態(tài)代理
動(dòng)態(tài)代理的原理是削锰,對(duì)a
對(duì)象的操作轉(zhuǎn)移到b
對(duì)象上來(lái)徒探,Ruby中使用delegate
庫(kù)來(lái)實(shí)現(xiàn)動(dòng)態(tài)代理。
示例代碼5
class UserProfile
def initialize(name)
@name = name
end
def hello
"#{@name} says hello."
end
end
class User < DelegateClass(UserProfile)
def initialize(user_profile)
super(user_profile)
end
end
user_profile = UserProfile.new("Rapheal")
user = User.new(user_profile)
p user.hello # 輸出 "Rapheal says hello."
關(guān)于respond_to?
respond_to?
是Ruby中用于判斷一個(gè)方法是否存在的一個(gè)方法喂窟。比如测暗,Class.respond_to?(:new) #返回true
,說(shuō)明Class
這個(gè)類可以調(diào)用new
方法磨澡。這個(gè)方法通常與define_method
碗啄、method_missing
等方法一起使用,與method_missing
一樣稳摄,不到萬(wàn)不得已稚字,不要修改這個(gè)方法。