Ruby是一門動態語言,動態創建與調用方法是其中一個體現。
動態方法
動態調用方法(動態派發)
動態調用方法,是指在代碼中不通過硬編碼而是在程序運行時自動去決定要調用的方法的一種行為。
示例代碼1
class Student
attr_accessor :name, :age, :birthday
def initialize(args = {})
name = args[:name]
age = args[:age]
birthday = args[:birthday]
end
end
【示例代碼1】
中initialize
方法中,給三個字段賦值的方式就是一種典型的硬編碼方式,假如這三個字段的名稱有改動,抑或添加、去掉字段的時候,不得不同時修改這個方法。為了避免這種情況,這里可以考慮使用動態調用的方式來重構它。
示例代碼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
通過【示例代碼2】
的重構,但凡attr_accessor
后面的字段有變動時,initialize
方法都會自動進行適配。那么實現的原理是什么呢?
在Ruby中,方法調用其實是向一個對象發送了一條消息,當接收方接收消息后,會在對象的祖先鏈中去尋找這個方法,找到之后調用它并返回給self
對象(詳細見【對象模型篇】)。也就是說,當調用str.method
的時候,本質上就是發送了一條方法調用的消息,接收者是str
對象,它等價于str.send(:method)
。因此示例代碼便很好理解了,它是將args
這個hash中的值進行遍歷,動態調用attr_accessor
生成的setter
和getter
方法。但有一個問題,如果參數中有在attr_accessor
未定義的字段怎么辦?比如Student.new({ year: 2016 })
,year
字段是未在attr_accessor
中定義的,如果調用self.year =
這個方法,是會拋異常的。所以這里添加了respond_to?
來判斷這個方法是否是存在的,存在再對它進行調用賦值。
動態定義方法
關于動態定義方法,其實在第一章對象模型篇的【示例代碼1】
已經在使用了,就是對define_method
的使用。在此基礎之上,此處實現一個更加具有可用性的案例:
示例代碼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
此處不再說明define_method
和attr_access
的使用,重點說明一下define_singleton_method
和cattr_access
的實現。
define_singleton_method
和define_method
的區別是,前者定義的是單例方法
(這里可稱為類方法),后者定義的是實例方法
。從用法來看,cattr_access
聲明的變量直接在類(這里是A
)上調用,而attr_access
聲明的變量需要在A
類對象實例化(A.new
)之后調用。同理,class_variable_set
和class_variable_get
定義的是單例變量(這里指類變量),而instance_variable_set
和instance_variable_get
定義的是實例變量。由于Ruby的語法約定,以@
開頭的為實例變量,以@@
開頭的為類變量,因此,在定義變量時尤其要注意變量的全名,否則會拋異常。
幽靈方法
還記得之前在方法查找中,如果找不到方法時,會觸發一個NoMethodError
的異常拋出。然而它來源于向對象發送了一個消息調用了一個方法叫做method_missing
。
假如對一個String
類對象str
調用test_method_a
,即str.test_method_a
,由于這個方法未定義,因此在祖先鏈中找不到這個方法。此時會發送一個消息str.send(:method_missing, :test_method_a)
,從而拋出NoMethodError
的異常。也就是說,當找不到要調用的方法時,會自動觸發調用method_missing
方法。那么如果重寫了某個類的method_missing
方法會是什么樣的結果呢?
示例代碼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
中對調用方法的名字做了限制,必須是html
、head
、title
、body
其中之一才會生成代碼,因此無需擔心其它額外正常調用不存在方法的時候不能正常拋出NoMethodError
異常的情況。由于在調用不存在的方法時就會調用method_missing
這個方法,因此如果要重寫這個方法一定要格外小心,能力越大,責任越大
。
幽靈方法與普通動態方法的優劣
普通動態方法是指,在類初始化時便使用define_method
等手段將需要的所有方法定義好。幽靈方法本質是在調用時,如果發現不存在方法時,那么即時定義這個方法并產生一次調用,從示例可以看出幽靈方法在定義方法時也是調用的define_method
等行為來定義動態方法。與普通定義動態方法的區別是,如果一個對象永遠沒有調用一個方法,那么這個方法永遠不會被定義,只有調用過一次時它才會被定義,因此使用幽靈方法時,對象所占用的內存空間比普通動態方法要少,反之付出的代價是第一次在祖先鏈中查找該方法的時間變長。這可以認為是一種以時間換取空間的策略。
動態代理
動態代理的原理是,對a
對象的操作轉移到b
對象上來,Ruby中使用delegate
庫來實現動態代理。
示例代碼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."
關于respond_to?
respond_to?
是Ruby中用于判斷一個方法是否存在的一個方法。比如,Class.respond_to?(:new) #返回true
,說明Class
這個類可以調用new
方法。這個方法通常與define_method
、method_missing
等方法一起使用,與method_missing
一樣,不到萬不得已,不要修改這個方法。