?
This document uses PHP Chinese website manual Release
當你寫的Ruby程序越來越大,越來越多之后,都會發(fā)現(xiàn)有很多代碼都可以重用,通常可以將相關例程組成一個庫,分布到不同的文件以便被其它Ruby程序共享。
通常,這些代碼都以類的形式組織在一起,所以你可能將一個類或者和其相關的幾個類放到一個文件中。
但是,有時候你也許需要把一些不能在常理上認為屬于一個類的東西組合到一起。
最初的辦法可能是將所有這些東西都放到一個文件中,然后在需要使用它的程序中引入這個文件,就像C語言那樣。但是,這樣做有一個問題,比如你編寫了一系列的三角函數(shù)sin,cos等,你將它們放到一個文件中,比如trig.rb,同時,Sally也做著類似的工作,創(chuàng)建了自己的庫文件action.rb,包括自己的一些例程,其中有beGood和sin方法。Joe想編寫一個程序,需要同時用到trig.rb和action.rb兩個文件,但是這兩個文件都定義了一個方法sin,這可不是什么好消息。
答案是模塊機制。模塊定義了一個命名空間,在這個空間里,你的方法和常量可以不必擔心和別人的重名,比如三角函數(shù)(trig)就可以放到一個模塊中:
module?Trig ??PI?=?3.141592654 ??def?Trig.sin(x) ???#?.. ??end ??def?Trig.cos(x) ???#?.. ??end end |
然后Sally的方法可以放到另一個模塊:
module?Action ??VERY_BAD?=?0 ??BAD??????=?1 ??def?Action.sin(badness) ????#?... ??end end |
模塊中常量的命名和類中的一樣,以大寫字母開頭。方法定義也類似,模塊的方法定義和類方法定義類似,格式為mod_name.method_name。
如果第三個程序需要使用這些模塊,只需要簡單的把這些模塊載入(使用Ruby的require語句,將在103頁討論),然后引用 限定的名字( qualified names)。
require?"trig" require?"action" y?=?Trig.sin(Trig::PI/4) wrongdoing?=?Action.sin(Action::VERY_BAD) |
和類方法一樣,調用一個模塊方法也以這個模塊名字為前綴然后一個點再加上方法名;引用一個模塊的則是在模塊后面加兩個冒號再加常量。
模塊還有一個非常有用的作用,即通過模塊使用叫做混合插入(mixin)的機制實現(xiàn)了多重繼承。
在上一節(jié)的例子里,我們定義了模塊方法,方法名前面加上了模塊名作為前綴。如果這樣讓你想到這是類方法,那么你可能會進一步想“如果我在模塊里面定義實例變量會怎樣呢?”這個問題非常好,一個模塊不能產生實例,因為它不是類。但是,你可以在一個類的定義里包含(include )一個模塊,這時候,這個模塊中所有的實例方法都變成了在這個類所擁有(能使用)的方法了(all the module's instance methods are suddenly available as methods in the class as well)。這就叫做mixin,實際上,mix-in很像超類(superclasses)。
module?Debug | ||
??def?whoAmI? | ||
????"#{self.type.name}?(\##{self.id}):?#{self.to_s}" | ||
??end | ||
end | ||
class?Phonograph | ||
??include?Debug | ||
??#?... | ||
end | ||
class?EightTrack | ||
??include?Debug | ||
??#?... | ||
end | ||
ph?=?Phonograph.new("West?End?Blues") | ||
et?=?EightTrack.new("Surrealistic?Pillow") | ||
| ||
ph.whoAmI? |
? |
"Phonograph?(#537766170):?West?End?Blues" |
et.whoAmI? |
? |
"EightTrack?(#537765860):?Surrealistic?Pillow" |
通過包含Debug模塊,Phonograph
和EightTrack
都能訪問whoAmI?
實例方法。
關于include語句需要注意幾點。首先,這個語句和文件無關,C程序員也使用預處理指令#include來將一個文件的內容在編譯的時候加入到另一個文件。而Ruby的include語句只是創(chuàng)建了一個指向一個有名字的模塊的引用,如果這個模塊在一個獨立的文件中,那么你必須先用require將這個文件引入,然后才能使用include。第二,Ruby的include不是簡單的將模塊的實例方法拷貝到類里面,而是建立一個從類到模塊的引用。如果很多個類都包含了同一個模塊,它們都指向同一樣東西。如果你修改了模塊中一個方法的定義,即使你的程序還在運行之中,你的類也能使用新的方法的行為[ 注意,我們這里說的是實例方法,實例變量永遠都是每個對象都有一份拷貝]
Mixin使得你能非常方便的給類增加方法,它的真正的強大之處在于能使模塊中的代碼和引入它的類中的代碼能相互作用。我們以標準的一個Ruby模塊Comparable
為例來說明,這個模塊可以用來給類增加比較操作符(<,<= ,==,>=,>等)和between?方法。為了使得它能工作,Comparable
假設使用它的類都定義了 <=>
操作符,所以作為類的創(chuàng)建者,你定一個<=>
方法,,引入Comparable
模塊,然后,你就免費得到了6個其它的方法。在我們的Song類里,我們比較的基準是歌曲的時長。我們需要做的是增加<=>
方法,和引入Comparable
模塊。
class?Song ??include?Comparable ??def?<=>(other) ????self.duration?<=>?other.duration ??end end |
然后,我們就可以檢查一下結果了,看看它的比較功能。
song1?=?Song.new("My?Way",??"Sinatra",?225) | ||
song2?=?Song.new("Bicylops",?"Fleck",??260) | ||
| ||
song1?<=>?song2 |
? |
-1 |
song1??<??song2 |
? |
true |
song1?==??song1 |
? |
true |
song1??>??song2 |
? |
false |
最后,再看一下我們第43頁實現(xiàn)的Smalltalk的inject方法,我們可以把它改得更通用一些,通過使用一個能mixin的模塊。
module?Inject ??def?inject(n) ?????each?do?|value| ???????n?=?yield(n,?value) ?????end ?????n ??end ??def?sum(initial?=?0) ????inject(initial)?{?|n,?value|?n?+?value?} ??end ??def?product(initial?=?1) ????inject(initial)?{?|n,?value|?n?*?value?} ??end end |
然后,我們就可以用一些內建類來測試一下:
class?Array | ||
??include?Inject | ||
end | ||
[?1,?2,?3,?4,?5?].sum |
? |
15 |
[?1,?2,?3,?4,?5?].product |
? |
120 |
class?Range | ||
??include?Inject | ||
end | ||
(1..5).sum |
? |
15 |
(1..5).product |
? |
120 |
('a'..'m').sum("Letters:?") |
? |
"Letters:?abcdefghijklm" |
更多的關于mixin的例子,可以參看403頁開始的關于Enumerable
模塊的文檔。
從C++轉到Ruby來的人經常問我們“在C++中,我們必須繞一些彎才能控制如何在多重繼承中共享實例變量,mixin中的實例變量會怎么樣呢?Ruby怎么處理這種情況呢?”
這對于初學者來說不是什么大問題,記住Ruby中的實例變量是如何工作的:以"@"作為前綴的變量作為當前對象self的實例變量。對于混合插入來說,你要引入的模塊可以在你的類里創(chuàng)建實例變量,并且可以用attr和friends來定義這些變量的訪問器(accessor )。比如:
(For a mixin, this means that the module that you mix into your client class (the mixee?) may create instance variables in the client object and may use attr and friends to define accessors for these instance variables. )?
module?Notes ??attr??:concertA ??def?tuning(amt) ????@concertA?=?440.0?+?amt ??end end class?Trumpet ??include?Notes ??def?initialize(tune) ????tuning(tune) ????puts?"Instance?method?returns?#{concertA}" ????puts?"Instance?variable?is?#{@concertA}" ??end end #?The?piano?is?a?little?flat,?so?we'll?match?it Trumpet.new(-5.3) |
Instance?method?returns?434.7 Instance?variable?is?434.7 |
我們不僅訪問了模塊中的方法,我們也可以訪問它的實例變量。但是,這樣也會有一定的風險,比如不同的模塊可能定義了一個同名的實例變量,從而產生了沖突。
module?MajorScales ??def?majorNum ????@numNotes?=?7?if?@numNotes.nil? ????@numNotes?#?Return?7 ??end end module?PentatonicScales ??def?pentaNum ????@numNotes?=?5?if?@numNotes.nil? ????@numNotes?#?Return?5? ??end end class?ScaleDemo ??include?MajorScales ??include?PentatonicScales ??def?initialize ????puts?majorNum?#?Should?be?7 ????puts?pentaNum?#?Should?be?5 ??end end ScaleDemo.new |
7 7 |
上面的兩個模塊中都定義了實例變量@numNotes
,當然程序最后的結果也與作者的期望的結果不同。
一般來說,mixin的模塊本身并不怎么攜帶實例數(shù)據(jù),它們使用訪問器(accessors )來從對象取得數(shù)據(jù)。但是如果你想要創(chuàng)建一個必須要有自己狀態(tài)的模塊,請確定這個模塊的實例變量的名字要唯一,不要和其它模塊的重名(比如用模塊名作為變量名的一部分)
你應該早就注意到了Ruby的集合類(collection classes)支持很多對它的處理:遍歷,排序等等,沒準你也會想要是自己的類支持這么多優(yōu)秀的特點就更好了。
當然,答案是肯定的,通過使用混合插入這個有用的機制和模塊Enumerable
,你只需要再寫一個名為each的迭代方法就可以了,在這個方法里,依次返回你自己的集合類的元素。混合插入Enumerable
模塊,然后你的類就支持了比如map,include?,find_all?等方法了。如果你的集合里的元素對象支持<=>方法,那么你的這個集合也可以得到min,max,sort方法。
?
因為使用Ruby輕松的能編寫優(yōu)良的模塊化的代碼,你會經常發(fā)現(xiàn)自己會寫一些包含自包含的代碼,比如面向x的接口,用于y的算法等。一般的時候,我們會把這些文件作為類或者模塊的庫。
有了這些文件,如果你想把它們整合到你的新程序中,Ruby提供了兩種方法:
load?"filename.rb" require?"filename" |
load方法每次執(zhí)行都會包含一個Ruby源文件,而require只會包含一個文件一次,而且,require還有別的功能,它還能裝載共享二進制的庫文件( load shared binary libraries)。這兩個方法都接收相對和絕對的文件路徑作為參數(shù),如果給的是相對路徑(或者只是一個文件名),系統(tǒng)將會在當前的裝載路徑(load path)中的每個文件夾下尋找這個文件(裝載路徑保存在全局變量$:中,見140頁的討論)
使用load和require包含的文件也可以包含其它文件。其中需要注意的是require是一個可執(zhí)行的語句,它可以在一個if語句里使用,or it may include a string that was just built。包含時候的查找路徑也可以在運行時候更改,你可以將需要的目錄加到$:,這是一個數(shù)組。因為load將無條件的裝載應該源文件,你可以用它在運行時候重新裝載一個可能在程序運行之后更改過的文件。
5.times?do?|i| ???File.open("temp.rb","w")?{?|f| ?????f.puts?"module?Temp\ndef?Temp.var()?#{i};?end\nend" ???} ???load?"temp.rb" ???puts?Temp.var ?end |
0 1 2 3 4 |