對(duì)象模型
所有class
定義之外的代碼默認(rèn)運(yùn)行在頂級(jí)對(duì)象main
中酥泛。
打開(kāi)類
ruby
的class
更像是一個(gè)作用于操作符而不是類定義語(yǔ)句,它可以創(chuàng)建一個(gè)不存在的類嫌拣,也可以打開(kāi)一個(gè)已定義的類柔袁,然后向內(nèi)添加新的方法和屬性,這種技術(shù)稱為“打開(kāi)類”技術(shù)异逐。
但是注意捶索,當(dāng)打開(kāi)類重新定義新的方法時(shí),如果跟該類已有的方法重名灰瞻,原來(lái)的方法就會(huì)被覆蓋腥例,這稱之為猴子補(bǔ)丁(MonkeyPatch
)
對(duì)象中有什么
實(shí)例變量
有如下類定義酝润。
class MyClass
def my_method
@v = 1
end
end
與java
這樣的靜態(tài)語(yǔ)言不同燎竖,ruby
對(duì)象的類和實(shí)例變量沒(méi)有關(guān)系,當(dāng)給實(shí)例變量賦值時(shí)他們才會(huì)生成要销,因此如果new
一個(gè)MyClass
類而沒(méi)有調(diào)用my_method
方法构回,就不會(huì)有@v
實(shí)例變量,這可以使用@instance_variables
方法驗(yàn)證
方法
可以通過(guò)#methods.grep /my/
來(lái)查找匹配my的方法
如果可以撬開(kāi)ruby
解釋器查看某個(gè)對(duì)象會(huì)發(fā)現(xiàn),對(duì)象其實(shí)并沒(méi)有包含一組方法纤掸,它只包含了實(shí)例變量和一個(gè)對(duì)自身的引用
重訪類
重要概念:類自身也是對(duì)象
類和其他對(duì)象一樣也有自己的類脐供,叫Class
(在java
和C#
中類也是Class
類的實(shí)例,Class
的類也是Class
借跪,自我引用)一個(gè)對(duì)象的方法就是類的實(shí)例方法政己,那么一個(gè)類的方法就是Class
的實(shí)例方法
"hello".class #=> String
String.class #=> Class
所有的類都最終繼承于Object
,Object
本身又繼承于BasicObject
(ruby
對(duì)象體系的根節(jié)點(diǎn))
String.superclass #=> Object
Object.superclass #=> BasicObject
BasicObject.superClass #=> nil
類是增加了三個(gè)方法的(new()
垦梆、allocate()
匹颤、superclass()
)Module
Class.superclass #=> Module
Module.superclass #=> Object
調(diào)用方法時(shí)發(fā)生了什么
方法查找
若有如下定義
Module M
def my_method
puts 'my method'
end
end
Class D
include M
end
Class C < D
end
C.new.my_method #可以使用C.new.ancestors打印祖先鏈
當(dāng)調(diào)用my_method
方法時(shí)會(huì)依據(jù)下圖的祖先鏈尋找該方法。注意Kernel
是Object
中包含的一個(gè)Module
托猩,所以所有的類都具有這個(gè)Module
中定義的實(shí)例方法(如print
)
我們也可以利用這種技術(shù)給Kernel
增加一個(gè)方法印蓖,這個(gè)內(nèi)核方法就可以為所有對(duì)象所用了。
self
每一行代碼都會(huì)在一個(gè)對(duì)象中被執(zhí)行——這個(gè)對(duì)象就是所謂的“當(dāng)前對(duì)象”京腥,當(dāng)前對(duì)象可以用self
來(lái)表示和訪問(wèn)赦肃,所有沒(méi)有指明接受者的方法都在self
上調(diào)用。
在類定義中當(dāng)前對(duì)象self
是在定義的類公浪,同時(shí)self
也是當(dāng)前類他宛,在對(duì)象的實(shí)例方法中self就是方法的接收者。
class Test
puts self #=> Test 當(dāng)前類
def foo
puts self
end
end
Test.new.foo #=> #<Test:0x007f8bf197b240> 當(dāng)前對(duì)象欠气,方法接收者
代碼 | 圖例 |
---|---|
image
|
image
|
左側(cè)的代碼有右側(cè)的祖先鏈厅各,所以Book.new.print
會(huì)調(diào)用printable:print
方法。每當(dāng)類包含一個(gè)模塊時(shí)预柒,該模塊會(huì)被插入到祖先鏈中队塘,在類的正上方
代碼 | 圖例 |
---|---|
image
|
image
|
Module extend self
extend self
的作用就是可以在不include
該模塊的情況下按如下方式調(diào)用
一般extend self
的模塊會(huì)被當(dāng)做一個(gè)只有類方法的工具類來(lái)使用
module M
extend self
def greet
puts "hi"
end
end
M.greet # => hi
總結(jié)
方法
動(dòng)態(tài)派發(fā)
使用class#send(:method_name, param)
調(diào)用方法的好處是可以在代碼運(yùn)行期知道最后一刻才決定調(diào)用那個(gè)方法。
動(dòng)態(tài)定義
使用class#define_method(: method_name, proc)
來(lái)定義一個(gè)方法
class MyClass
define_method :my_method do |arg|
arg * 3
end
endMyClass.new.my_method(3) #=> 9
動(dòng)態(tài)刪除
使用class#undef_method
刪除方法宜鸯,從而得到一個(gè)白板類憔古。
class MyClass
undef_method puts
puts "hello" #=> 由于刪除了puts方法,該句會(huì)報(bào)錯(cuò)
end
幽靈方法(慎用)
所有在類中沒(méi)定義的方法都會(huì)調(diào)用Kernal
中的method_missing()
方法淋袖,該方法拋出NoMethodException
異常鸿市,可以重寫該方法來(lái)收集那些“迷路“的方法調(diào)用
class MyClass
def method_missing(method, *args)
puts "you call: #{method}(#{args.join(',')})"
end
end
可以定義一個(gè)類,只要用等號(hào)給它賦值一個(gè)屬性即碗,該對(duì)象就會(huì)自動(dòng)擁有該屬性
class No
def initialize
@attr = {}
end
def method_missing(method, *args)
if method =~ /=$/
@attr[method.to_s.chop] = args[0]
else
@attr[method.to_s]
end
end
end
n = No.new
n.name = "lang"
n.age = 27
代碼塊
yield
可以通過(guò)yield
將原代碼塊執(zhí)行焰情,并做一些附加操作
class Scanner
def read_file
using(f = File.new) {
f.read(file_path)
f.close
}
end
# 定義using關(guān)鍵詞即便出現(xiàn)異常也能自動(dòng)關(guān)閉資源
def using(resource)
begin
yield
rescue
resource.close
end
end
end
作用域
一般情況下
局部變量只能存在當(dāng)前作用域中,也就是說(shuō)局部變量無(wú)法跨作用域調(diào)用剥懒,一般情況下程序會(huì)在以下三個(gè)作用域門關(guān)閉前一個(gè)作用域烙样,同時(shí)打開(kāi)一個(gè)新的作用域
類定義
模塊定義
方法定義
實(shí)例變量會(huì)在實(shí)例的生存周期中都可見(jiàn)
扁平作用域
如果想讓局部變量(綁定)穿過(guò)作用域門,則可以用方法調(diào)用來(lái)替代作用域門蕊肥,具體來(lái)說(shuō)就是用Class.new
代替class
關(guān)鍵字,Module.new
代替module
關(guān)鍵字,define_method
代替def
關(guān)鍵字
count = 1
Counter = Class.new do
define_method :get_count do
count
end
end
c = Counter.new
puts c.get_count
#instance_eval
使用該方法傳入一個(gè)代碼塊壁却,塊的接受者會(huì)成為self
批狱,因此代碼塊中可以訪問(wèn)接受者的私有方法和實(shí)例變量
class Test
attr_accessor :name
def initialize
@name = "lang"
end
end
t = Test.new
t.instance_eval do
@name = "zhou"
end
puts t.name #=> zhou
可調(diào)用對(duì)象
proc對(duì)象
盡管ruby中絕大部分東西都是對(duì)象,但是塊不是展东,如果想存儲(chǔ)一個(gè)塊供以后使用需要一個(gè)對(duì)象才能做到赔硫,為了解決這個(gè)問(wèn)題,ruby在標(biāo)準(zhǔn)庫(kù)中提供了Proc
類盐肃,可以通過(guò)將代碼塊傳給Proc.new()
方法來(lái)創(chuàng)建一個(gè)proc
爪膊,然后調(diào)用Proc#call
來(lái)執(zhí)行,這種技術(shù)稱為延遲執(zhí)行
#第一種方法
inc = Proc.new {|x| x + 1} #=> Proc對(duì)象
inc.call(2) #=> 3
#第二種方法
dec =lambda {|x| x - 1} #=> Proc對(duì)象
dec.call(2) #=> 1
給一個(gè)方法傳入代碼塊的方式有兩種:
第一種砸王,在方法末尾直接聲明代碼塊推盛,然后在方法內(nèi)部通過(guò)yield
調(diào)用,但缺點(diǎn)是不能重復(fù)使用yield
(因?yàn)槠涫谴a塊而不是對(duì)象)谦铃,比如再將yield
傳給方法內(nèi)部的方法
def foo(greeding)
puts "#{greeding} #{yield}"
end
foo("hello"){"lang"}
# or
foo("hello", &(Proc.new {"lang"}))
第二種耘成,將Proc
作為參數(shù)傳入方法中,可以通過(guò)&符號(hào)將Proc
對(duì)象轉(zhuǎn)換為代碼塊驹闰,作為最后一個(gè)參數(shù)瘪菌,然后通過(guò)Proc#call
調(diào)用
&的真正含義是,這是一個(gè)Proc
對(duì)象嘹朗,我想把它當(dāng)做代碼塊來(lái)使用师妙,簡(jiǎn)單的去掉&操作符,就能得到一個(gè)proc
對(duì)象
def foo(greeding, &name)
puts "#{greeding} #{name.call}"
end
zhou = Proc.new {"zhou"}
foo("nihao", zhou)
類定義
.class_eval
如果想要在不知道類名的情況下打開(kāi)類并且使用def定義一個(gè)方法屹培,可以使用.class_eval
方法默穴,如果想要打開(kāi)一個(gè)對(duì)象(修改self
)可以使用#instance_eval
方法
def def_method_for_class(a_class)
a_class.class_eval do
def say_hi
puts "hi"
end
end
end
def_method_for_class(String)
'abc'.say_hi
類實(shí)例變量和類變量
由于類也是對(duì)象,所以在類定義的作用域中如果定義@var
(@同self)惫谤,則該變量是類的實(shí)例變量壁顶,類實(shí)例變量不能在實(shí)例方法中調(diào)用
class Test
@var = 2 #=> 類的實(shí)例變量
def self.read
@var
end
def set_var
@var = 1 #=> 對(duì)象的實(shí)例變量
end
def get_var
@var
end
end
t = Test.new
t.set_var
puts Test.read #=> 2
puts t.get_var #=> 1
可以使用@@var
定義類變量恤煞,類變量可以在實(shí)例方法中訪問(wèn)
class Test
@@var = 2 #=> 類的實(shí)例變量
def get_var
@@var #=> 2
end
end
單件方法
只給某個(gè)對(duì)象增加一個(gè)方法验懊,則這個(gè)方法叫做單件方法
str = "nihao"
def str.title?
if self.upcase == self
end
其實(shí)我們?nèi)粘J褂玫念惙椒ň褪穷悓?shí)例的單件方法妥色,而且其定義方式也跟單件方法一樣
def MyClass.singleton_method; end
def str.singleton_method; end
類宏
如果類MyClass中有一批舊方法如 old_method1, old_method2已經(jīng)棄用仰担,對(duì)其的調(diào)用希望實(shí)際調(diào)用新方法new_method1, new_method2令杈,如何優(yōu)雅的解決锁蠕?
class MyClass
#def old_method1; puts "old_method1"; end
#def old_method2; puts "old_method2"; end
def new_method1; puts "new_method1"; end
def new_method2; puts "new_method2"; end
def self.replace_method(old_method, new_method)
warn "#{old_method}已棄用,請(qǐng)用新方法#{new_method}"
define_method :old_method do |*args, &block|
send(new_method, *args, &block)
end
end
replace_method :old_method1, :new_method1 #=> self.replace_method(:old_method1, :new_method1)
replace_method :old_method2, :new_metho2d
end
用元編程實(shí)現(xiàn)的attr_accessor
class MacroClass
def self.prop(name)
define_method name do
instance_variable_get "@#{name}"
end
define_method name.to_s + "=" do |value|
instance_variable_set "@#{name}", value
end
end
prop :name #=> 等價(jià)于attr_accessor
def initialize
@name = "zhou"
end
end
eigenclass
每個(gè)eigenclass
只有一個(gè)實(shí)例并且不能被繼承养葵,它是對(duì)象的單件方法的存活之處入客。在調(diào)用一個(gè)方法時(shí)自阱,接收者會(huì)先查詢eigenclass
中有沒(méi)有該方法(單件方法)嚎莉,如果有就直接調(diào)用,如果沒(méi)有就沿著祖先鏈一直向上尋找沛豌。
對(duì)于類的eigenclass
就是存放類方法的地方
下圖闡釋了eigenclass
在祖先鏈的位置 以#開(kāi)頭的為eigenclass
eigenclass
的超類就是超類的eigenclass
趋箩,有了這種繼承關(guān)系赃额,可以在子類中調(diào)用父類的類方法(因?yàn)?D繼承#C)一個(gè)對(duì)象的
eigenclass
類的超類是這個(gè)對(duì)象的類,一個(gè)類的eigenclass
的超類是該類的超類的eigenclass
打開(kāi)對(duì)象的
eigenclass
定義單件方法
# 打開(kāi)obj的enginclass,定義一個(gè)單件方法a_singleton_method
# 如果把obj換成類名,或在類定義中使用 class << self 則打開(kāi)該類的eigenclass添加屬性
class << obj
def a_singleton_method
'obj#a_singleton_method'
end
end
打開(kāi)類的eigenclass定義類方法
# 打開(kāi)obj的enginclass,定義一個(gè)單件方法a_singleton_method
# 如果把obj換成類名,或在類定義中使用 class << self 則打開(kāi)該類的eigenclass添加屬性
class C; end
class << C
def class_method
"C.class_method"
end
end
# 或
class C
class << slef
def class_method
"C.class_method"
end
end
end
類擴(kuò)展和對(duì)象擴(kuò)展Object#extend
extend
關(guān)鍵字可以代替include
關(guān)鍵字叫确,用于將模塊混含到類或?qū)ο笾刑迹ㄒ徊煌氖牵褂?code>extend會(huì)使模塊方法變成對(duì)象的單件方法竹勉,或成為類的類方法
module MyModule
def say_hi; puts "hi" end;
end
obj = Object.new
obj.extend MyModule
obj.say_hi #=> 成為對(duì)象的單件方法
class MyClass
extend MyModule
end
MyClass.say_hi #=> 成為類方法
方法別名alias和環(huán)繞別名
可以使用alias
關(guān)鍵字給方法定義別名
module MyModule
def say_hi; puts "hi" end;
alias :hi :say_hi #=> 注意兩個(gè)方法名之間沒(méi)有逗號(hào)
end
如果想要對(duì)某一個(gè)我們不能修改的庫(kù)方法前后增加額外代碼飞盆,而這個(gè)庫(kù)方法在項(xiàng)目中已經(jīng)用過(guò)無(wú)數(shù)次,我們不能修改每一處調(diào)用該如何處理次乓?
這時(shí)可以使用環(huán)繞別名的技巧吓歇,該技巧的核心是:如果先定義別名再修改方法,則使用別名調(diào)用的時(shí)候還是調(diào)用的老方法票腰,這樣我們就可以先用別名把老方法存下來(lái)城看,然后重新定義這個(gè)方法,加上額外處理的代碼后丧慈,再使用別名調(diào)用老方法
class String
alias :real_length :length
def length
if self.real_length > 5 #=> 使用原來(lái)的方法
"long"
else
"short"
end
end
end
puts "abc".real_length #=> 3
puts "abc".length #=> short
類擴(kuò)展混入
上文說(shuō)到析命,如果想將一個(gè)module
中的方法當(dāng)做類的實(shí)例方法包含進(jìn)來(lái),可以使用include
關(guān)鍵字
如果想當(dāng)做類方法包含進(jìn)來(lái)逃默,則可以使用extend
關(guān)鍵字鹃愤,或者將該模塊包含到類的eigenclass
中(class << self; ....; end
)
class Myclass
include MyModule #=> 作為實(shí)例方法包含進(jìn)類
extend MyModule #=> 作為類方法包含進(jìn)類
class << self
include MyModule #=> 作為類方法包含進(jìn)類
end
end
module MyModule
#...
end
但是如果想部分當(dāng)做實(shí)例方法,部分當(dāng)做類方法mixin
到類中如何操作呢
在module
中創(chuàng)建一個(gè)類ClassMethods
完域,該類中包含想要定義成類方法的方法
在included
鉤子方法中使用extend
方法將ClassMethods
類中的方法包含到包含者的eigenclass
中(成為類方法)
在ClassMethods
類外的方法被include時(shí)還是實(shí)例方法
將該module include
到類中
class Myclass
include MyModule
end
module MyModule
# 鉤子方法,當(dāng)模塊被混含時(shí)調(diào)用,base為包含模塊的類
def self.included(base)
base.extend(ClassMethods) #=> extend方法會(huì)把ClassMethod類中的方法包含到base的eigenclass中
end
# 成為包含者的實(shí)例方法
def instance_method
end
# 該類中的方法成為包含者的類方法
class ClassMethods
def xxx
#...
end
end
end
測(cè)試
相對(duì)于測(cè)試普通代碼软吐,測(cè)試元編程代碼引入了額外的維度,記住吟税,元編程是編寫代碼的代碼凹耙,因此你可能需要在兩個(gè)層次上測(cè)試它
- 測(cè)試自己的代碼
- 測(cè)試這個(gè)代碼所生成的代碼