最近復(fù)習(xí)了下 ruby 對象模型的知識,參照了 Ruby Metaprogramming昏兆,于是邊看邊做筆記,還是收獲很多。
Open Class
class String
def my_method
"#{self} length is #{self.length}"
end
end
class 更像是一個作用于操作符意系,而不是聲明語句。
創(chuàng)建類是一個令人愉快的副作用饺汹。
class 關(guān)鍵字的核心任務(wù)式把你帶到類的上下文中蛔添,讓你可以在里面定義方法。使用它可以打開類兜辞,進(jìn)行動態(tài)修改迎瞧。
Inside the Object Model
對象包含實例變量,可以使用 Object#instance_variables
查看
類的名字是一個常量
對象的類和實例變量并沒有關(guān)系逸吵,給它賦值的時候凶硅,它們就出現(xiàn)了。每個對象的實例變量不同扫皱。
解釋器中足绅,一個對象僅僅包含它自己的實例變量,以及一個對自身 Class 的引用啸罢。
為了共享编检,方法必須存放在類中,而非對象中扰才。
String.instance_methods == 'aaa'.methods # true
String.methods == String.instance_methods # false
類本身就是對象,是 Class 類的實例厕怜。
Class.instance_methods(false) # [:allocate, :new, :superclass]
如果你希望代碼被 include 進(jìn)去衩匣,就用模塊,如果你希望某段代碼被實例化或者繼承粥航,就應(yīng)該使用類琅捏。
[].class.class # Class
[].superclass # Object
Constant and path
幫助我們找到常量、類等递雀。:: 表示 root-level柄延。
module M1
class C1
X = 'constant'
end
C1::X
end
M1::C1::X
:: # root-level
Y = 'root-level constant'
module M
Y = 'constant in M'
Y
::Y
end
Module.nesting
表示當(dāng)前路徑。
Objects and Classes 小結(jié)
什么是對象缀程,對象就是一組實例變量和一組指向其類的引用搜吧。
對象的方法并不存在與對象本身,而是存在與對象的類中杨凑。在類中滤奈,稱為實例方法。
類是一個對象 ( Class 類的一個實例)外加一組實例方法和一個對超類(Superclass)的引用撩满。Class 類是 Module 類的子類蜒程,所以類也是一個模塊绅你,但是你不能 include,也不能 prepend 類昭躺。
以下一個很著名的 pattern忌锯,用來實現(xiàn)類,可以思考這樣做的目的是什么领炫。
module Hello
module ClassMethods
def class_m
'ClassMethods'
end
end
module InstanceMethods
def instance_m
'InstanceMethods'
end
end
def self.included(receiver)
receiver.extend ClassMethods
receiver.send :include, InstanceMethods
end
end
class C
include Hello
end
Method lookup
ruby 進(jìn)行方法查找遵循向右偶垮,向上的原則。
receiver 是調(diào)用方法的對象驹吮,明確方法的 receiver 是非常重要的针史。
ancestors 是祖先鏈,表示繼承關(guān)系碟狞。ruby 先在接受者中查找方法啄枕,再沿著祖先鏈向上查找,直到找到為止族沃。
module M1, Class C,
C include M1
Class D < Class C
D.ancestors # [C, M1, Object, Kernel, BaseObject]
M1 M2 誰 include 在前频祝,就在 ancestors 的前面。
C include M1
C include M2
C.ancestors # [C, M2, M1, Object, Kernel, BaseObject]
而 prepend 將會導(dǎo)致如下:
C prepend M1
C.ancestors # [M1, C, Object, Kernel, BaseObject]
如果一個類或者模塊已經(jīng)出現(xiàn)在祖先鏈中脆淹,將會忽略 prepend 與 include常空。
Kernel Module 提供內(nèi)核方法,被 Object include盖溺。你當(dāng)然可以用它做點“壞事”漓糙。
Self
Ruby 中每一行代碼都會在一個對象中被執(zhí)行,當(dāng)前對象用 self 表示烘嘱,也可以用 self 對其進(jìn)行訪問昆禽。
private 關(guān)鍵字只能被隱形的 self 調(diào)用。
私有規(guī)則:如果接受者不是自己蝇庭,就必須指明接受者醉鳖;私有方法只能被隱性的接受者調(diào)用;
私有方法可以被繼承哮内。
# main - top level context
irb -> self # => main
self.class # => Object
一個私有規(guī)則的例子:
class Klass
def method_a
method_b
end
private
def method_b
'hi'
end
end
Klass.new.method_a
class Klass
def method_a
self.method_b
end
end
Klass.new.method_a
# NoMethodError: private method `method_b' called for #<Klass:0x007fd0d50ad428>
因為不滿足私有規(guī)則盗棵,所以丟出 NoMethodError 的錯誤。
在上面這個例子中北发,如果不使用 self.class
的話纹因,會發(fā)生什么后果?
class Point
attr_reader :x, :y
def initialize(x, y)
@x, @y = x, y
end
def +(other)
self.class.new(@x + other.x, @y + other.y)
end
def -(other)
self.class.new(@x - other.x, @y - other.y)
end
def to_s
"{#{@x}, #{@y}}"
end
end
Refine
Refine 是用來代替猴子補丁的一種做法鲫竞,也不能完全替代猴子補丁辐怕。
module StringExtensions
refine String do
def my_length
"#{self} length is #{self.length}"
end
end
end
module StringStuff
using StringExtensions
'foo'.my_length
end
在兩種情況下會起到作用:
- 在
refine block
內(nèi)部 - 從
using
開始到模塊結(jié)束,或者文件結(jié)束
這里有個很有意思的例子說明 refine从绘,可以猜猜輸出是什么寄疏。
class MyClass
def my_method
'original'
end
def another_method
my_method
end
end
module MyClassRefine
refine MyClass do
def my_method
'refined my_method'
end
end
end
module Run
using MyClassRefine
p MyClass.new.my_method # ???
p MyClass.new.another_method # ???
end
Dynamic Methods & Dispatch
Object#send
Object#public_send
幫助我們實現(xiàn)了 dynamic dispatch是牢,是一種很好用的反射方式。
Module#define_method
幫助我們動態(tài)的創(chuàng)建方法陕截。書上最后的例子是非常值得學(xué)習(xí)的驳棱,請牢記我的 comments。
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
data_source.methods.grep(/^get_(.*)_info/) do # 使用內(nèi)省的方式自動創(chuàng)建农曲,進(jìn)行部分解耦
self.class.define_component $1 # 使用 self.class 避免子類丟失方法
end
end
def self.define_component(name)
define_method(name) do
# dynamics dispatcher # 動態(tài)的調(diào)用
info = @data_source.send "get_#{name}_info", @id
price = @data_source.send "get_#{name}_price", @id
result = "#{name.capitalize}: #{info} (#{price})"
price >= 100 ? "* #{result}" : result
end
end
end
Ghost Methods
BacisObject#method_missing
是當(dāng)沒有按照向右社搅,向上查找后調(diào)用,參數(shù)為 (method, *args, &block)乳规,Ruby 很靈活形葬,這個表示如果沒有找到你的方法,那就調(diào)用這個暮的,所以可以 override 這個家伙幫我們制作那些并不存在于 Object#methods
的方法件甥。
BacisObject#method_missing
也可以用其來實現(xiàn)動態(tài)的方法調(diào)用盯腌,注意不是定義凌埂。
所以 ghost method 并不是真正的方法触幼,我們知道當(dāng)你通過 Object#response_to?
查詢一個方法時,將會返回 true恨闪,而對于 ghost method倘感,將會返回 false。而 response_to 將調(diào)用 response_to_missing咙咽,所以千萬不要忘記要重現(xiàn) response_to_missing老玛。
我非常喜歡下面的例子,可以想想如果沒有 number = 0 將會發(fā)生什么钧敞。Ghost Methods 十分強大逻炊,所以一定要小心,don't break anything犁享。
def method_missing(name, * args)
person = name.to_s.capitalize
super unless is_our_member? person
number = 0
3.times do
number = rand(10) + 1
puts "#{number} ..."
end
"#{person} got a #{number}"
end
Ghost Methods 產(chǎn)生風(fēng)險的原因是他們并非真正的方法,只是對方法的攔截豹休。你有很多規(guī)避放的地方炊昆,比如必須要調(diào)用 super;還需要更新 responsd_to_missing?威根。使用時凤巨,牢記那個有意思的 bug。
Dynamic Methods 就是真正的方法洛搀,只是定義的方式不一樣罷了敢茁。
某時,你只能使用 Ghost Methods留美,例如 JSON 庫彰檬,你無法確定有多少種標(biāo)簽?zāi)阈枰С稚烊小τ?DAO 的話,根據(jù) table schema 可以簡單的使用 define_method 生成你想要的方法逢倍。
在可以使用動態(tài)方法的時候捧颅,使用;除非必須使用 Ghost Methods较雕,否則盡量不使用碉哑。
Blocks Are Closures
代碼塊即包含了代碼,也包含了一組綁定亮蒋。代碼庫在定義時扣典,獲得定義中的綁定,運行時慎玖,帶著綁定進(jìn)入該方法贮尖。
def my_method
x = 'good bye'
yield('cruel')
end
x = 'hello'
my_method {|y| "#{x},#{y} world" }
代碼塊獲取局部綁定,一直攜帶著這些綁定凄吏。在 JavaScript 中远舅,我們往往這樣描述閉包:閉包是一個擁有許多變量和綁定了這些變量的環(huán)境的表達(dá)式(通常是一個函數(shù)),因而這些變量也是該表達(dá)式的一部分痕钢。牢記執(zhí)行的代碼以及攜帶的綁定可以幫你解決很多問題图柏。
提到閉包,就不能不提到作用域 Scope任连。只要程序切換了 Scope蚤吹,有些綁定就會被新的綁定所取代。
程序會在三個地方關(guān)閉前一個作用域随抠,同時打開一個新的作用域:類定義裁着,模塊定義,方法拱她。
在 class/module 與 def 之間還有一個小區(qū)別二驰,在類定義與模塊定義中的代碼會立刻執(zhí)行(這是很多 Java 程序員很難理解的地方),例如:
class B
def self.some
'bar'
end
some #=> bar
end
下面使用 Class.new 中的 block 穿越作用域秉沼,來達(dá)到扁平作用域的作用桶雀,使用閉包穿越作用域 ,在 Class.new 中唬复,調(diào)用到另一個作用域里的變量矗积。可以試試如果使用 class
關(guān)鍵字敞咧,該怎么辦棘捣。
my_var = "Success"
MyClass = Class.new do
puts "#{my_var} in class"
define_method :my_method do
puts "#{my_var} in my_method"
end
end
MyClass.new.my_method
Proc & Lambda
你可以把 block
傳遞給 Proc.new
方法來創(chuàng)建一個 Proc
對象,然后使用 Proc#call
來執(zhí)行休建,技巧叫做 Deferred Evaluation乍恐,也叫做 Lazy Loading评疗。如果你對函數(shù)式編程比較熟悉的話,這個概念應(yīng)該不陌生禁熏。
有兩個內(nèi)核方法可以把 block 轉(zhuǎn)換為 Proc:proc & lambda壤巷。
p = lambda { |x| x + 1 }
p = -> (x) { x + 1} # 簡寫
& 操作符的意思是,這是一個 block 對象瞧毙,我想把它當(dāng)做 block 來使用胧华,去掉 &,就會得到一個 block 對象宙彪,例如:
class C
def foo
'foo'
end
def bar(&block)
block.call
end
end
c = C.new
m = c.method :foo
c.bar m # error!
c.bar &m # to block
c.bar do
'hi'
end # hi
proc 和 lambda 的區(qū)別:
區(qū)別1: return 是從定義 proc 的作用域中返回矩动, lambda 則表示在 lambda 中返回。
區(qū)別2: lambda 的參數(shù)數(shù)量是嚴(yán)格的释漆,更像是方法悲没。
def method_a(callable)
callable.call * 2
end
p = proc { return 10 }
method_a p # error
p = proc { 10 }
method_a p
p = lambda { return 10 }
method_a p
Method 對象
可以使用 Object#method
方式獲取 method 對象。
method 對象是可以被執(zhí)行的男图,Method#call
將會執(zhí)行示姿,參看 Duck Typing。method 對象可以通過 Method#to_proc
轉(zhuǎn)為 Proc逊笆。
proc栈戳,lambda 是在定義的作用域中執(zhí)行,而 method 對象是在自身所在對象的作用域中執(zhí)行难裆。
class Foo
def bar(&callable)
callable.call
end
def some_method
x
end
def x
'xxx in Foo'
end
end
x = 'xxx'
m = proc do
x
end
f = Foo.new
f.bar &m
f.bar &(f.method :some_method)
Class Definition Demystified
我們可以在類定義中放入任何代碼子檀,最后一條語句就是返回值。
在定義(模塊)時乃戈,類本身就是當(dāng)前對象的 self褂痰,類和模塊也是對象,所以類是可以充當(dāng) self 的症虑。self 是當(dāng)前的對象缩歪。
- 在程序頂層,當(dāng)前類是 Object谍憔,是 main 對象所屬的類
- 在方法中驶冒,當(dāng)前類就是當(dāng)前對象的類。
- 使用 class 或者 module 打開時韵卤,所打開的類是當(dāng)前類。
class C
def m1
def m2
end
end
end
class D < C
end
c.instance_methods false # => :m1 在此時崇猫,并沒有執(zhí)行 m1, 則只有 m1
D.new.m1 # => 執(zhí)行了 m1, 但是 m2 定義在了 C 上
c.instance_methods false # => :m1, :m2
另一個例子:
def add_method(klass)
puts self # => main
klass.class_eval do
puts self # => String or other class
def m
puts self # => instance
'hello'
end
end
end
add_method String
- 所有使用 def 定義的方法都是當(dāng)前類的實例方法
- 在類的定義中沈条,當(dāng)前類就是 self —— 正在定義的類
- 如果有引用,則可以使用 class_eval 打開這個類诅炉,self 則會被修改
Ruby 解釋器假定所有的實例變量都屬于當(dāng)前對象 self蜡歹,所以在類定義中屋厘,我們是可以使用 @ 的。@x 為類實例變量 Class Instance Variables月而。只能被自身訪問汗洒,不能被類的實例或者子類所訪問。
class MyClass
@x = 1
def self.read
@x
end
end
MyClass.read
Singleton Methods & Objects
我們之前提到父款,可以單獨的為一個對象增加獨有的方法溢谤,例如:
str = 'sss'
def str.foo
"bar #{self}"
end
現(xiàn)在問題來了,這個方法在哪里憨攒?按照之前提到的方法查找世杀,我們應(yīng)該去查找的是 String#foo
,但是其中并沒有定義這個方法肝集。答案是瞻坝,這個方法會被放在了 單例類 中。
那么以前我們提到的類方法是什么呢杏瞻?也許你已經(jīng)明白了所刀,類是一個對象,是 Class 的對象捞挥。所以類方法的本質(zhì)是:它就是一個類(class 對象)的單件方法浮创。
class C
def a_method
'C#a_method'
end
end
class D < C
end
obj = D.new
p obj.a_method # => C#a_method
class << obj
def a_singleton_method
'obj#a_singleton_method'
end
end
p obj.a_singleton_method # => "obj#a_signleton_method"
p obj.singleton_class # => #<Class:#<D:0x007fe5c605ec50>>
p obj.singleton_class.superclass #=> D
class C
class << self
def a_class_method
'C.a_class_method'
end
end
end
p C.a_class_method # => 'C.a_class_method'
p D.a_class_method # => 'C.a_class_method'
p C.singleton_class # => #C
p D.singleton_class # => #D
p D.singleton_class.superclass # => #C
p C.singleton_class.superclass # => #Object
所以我們修改了方法查找:
Object.singleton_class => #Object
Object.singleton_class.superclass => #BaseObject
Object.singleton_class.superclass.superclass.class => Class
歸根結(jié)底,我們補充了 Ruby 的對象模型树肃,并且給出了最重要的 7 條規(guī)則:
- 只有一種對象——要么是普通對象蒸矛,要么是模塊或者類
- 只有一種模塊——普通模塊、一個類或者單件類
- 只有一種方法胸嘴,存在于模塊中——通常在類中
- 每個對象都有自己的“真正的類”——要么是一個普通類雏掠,要么是一個單件類
- 除了 BaseObject 沒有超類,每個類都有且只有一個祖先
- 一個對象的單件類的超類是這個對象的類劣像,一個類的單件類的超類是這個類的超類的單件類
- 調(diào)用一個方法乡话,Ruby 先向右去找接收者真正的類,再向上查找