一、心得體會
1、今天完成了什么?
- 今天看了20頁的鎬頭書(139-160)
- 看了10個controller
2、今天收獲了什么?
- Ruby中的循環有哪些?
- 捕獲、異常和拋出是怎么工作的?
- 含有異常信息的數據包(package)是Exception類或其子類的一個對象。
- Ruby將相關的Exception對象的引用放在全局變量$!中,這與任何隨后的異常處理不相干。不帶任何參數的調用raise,它會重新引發$!中的異常。舉個栗子:
op_file = File.open(opfile_name, "w")
begin
# 這段代碼引發異常會被下面的rescue語句捕獲
while data = socket.read(512)
op_file.write(data)
end
rescue SystemCallError
$stderr.print "IO failed: " + $!
op_file.close
File.delete(opfile_name)
raise
end
- 10個controller:取送、取送日志、取送跟進、取送跟進、疑難原因、區縣(districts)、禮物(gifts)、催單(hastens)、盤點(inventories)、盤點訂單
3、今天犯了哪些錯誤?
4、明天需要做哪些工作?
二、讀書筆記
昨天我學到的最重要的是什么?
Ruby有幾種循環:
- while
- until
- 迭代器循環,如times、upto(遞增)、downto(遞減)、each、step
- For..in
7.6.3 Break、Redo和Next
循環控制結構break,redo和next可以讓你改變循環或者迭代的正常流程。
- break終止最接近的封閉循環體,然后繼續執行block后面的語句。
- redo從循環頭重新執行循環,但不重計算循環條件表達式或者獲得迭代中的下一個元素。
- next跳到本次循環的末尾,并開始下一次迭代
while line = gets
next if line =~ /^\s*#/ # skip comments
break if line =~/^END/ # stop at end
redo if line.gsub!(/'(.*?)'/) { eval($1) }
# process line ...
end
這些關鍵字還可以和任何基于迭代的循環機制一起使用。
i = 0
loop do
i += 1
next if i <3
print i
break if i > 4
end
輸出結果:
345
7.6.4 Retry
redo語句使得一個循環重新執行當前的迭代,但是有時你需要從頭重新執行一個循環,retry語句就是做這件事的,它重新執行任意類型的迭代式循環。
for i in 1..100
print "Now at #{i}. Restart? "
retry if gets =~ /^y/i
end
交互式地運行這段代碼,你會看到:
Now at 1. Restart? n
Now at 2. Restart? y
Now at 1. Restart? n
retry在重新執行之前會重新計算傳遞給迭代的所有參數。下面是一個DIY的until循環的例子。
def do_until(cond)
break if cond
yield
retry
end
i = 0
do_until(i>10) do
print i, " "
i += 1
end
7.7 變量作用域、循環和Blocks
while、until和for循環內建到了RUby語言中,但沒有引入新的作用域,前面已經存在的局部變量可以在循環中使用,而循環中新創建的局部變量也可以在循環后使用。
被迭代器使用的block(比如loop和each)與此略有不同,通常,在這些block中創建的局部變量無法在block外訪問。
[1, 2, 3 ].each do |x|
y = x + 1
end
[x, y]
輸出結果:
NameError: undefined local variable or method `y' for main:Object
from (irb#1):126
然而,如果執行block的時候,一個局部變量已經存在且與block中的變量同名,那么block將使用此已有的局部變量,因而,它的值在塊后面仍然可以使用。
如下面的例子所示,此即適用于block中的普通變量也適用于block的參數。
x = nil
y = nil
[1, 2, 3].each do |x|
y = x + 1
end
[x, y] -> [3, 4]
為什么終端結果是這樣的:
[nil, 4]
注意在外部作用域中變量不必有值:Ruby解釋器只需要看到它即可。
if false
a = 1
end
3.times {|i| a = i}
第8章
異常、捕獲和拋出(Exceptions,Catch and Throw)
到目前為止,我們都是在歡樂谷中開發代碼,每個程序庫的調用都是成功的,用戶從來沒有輸入不準確的數據。但是,在現實世界中,錯誤會發生,好的程序可以預見它們的發生,然后優雅地處理這些錯誤,但這并非總像聽起來那么簡單,通常檢測到錯誤出現的那部分代碼,缺少有關如何處理它的上下文信息。
舉個栗子:
試圖去打開一個并不存在的文件,在一些情況下是可行的的,但在別的情況下卻是致命的錯誤,文件處理模塊要如何做呢?
傳統的做法是使用返回碼。open方法在失敗時會返回一些特定值,然后這個值會沿著調用例程的層次往回傳播,直到有函數想要處理它。
這種做法問題是,管理所有這些錯誤碼是一件痛苦的事情。如果函數首先調用open,然后調用read,最后調用close方法,而且每個方法都有可能返回錯誤標識,那么當函數將返回碼返回給調用者時,該如何區分這些錯誤碼呢?
異常在很大程度上解決了這個問題,異常允許把錯誤信息打包到一個對象中,然后該異常對象被自動傳播調用棧(calling stack),直到運行系統找到明確聲明直到如何處理這類異常的代碼為止。
8.1 異常類(The Ecxeption Class)
含有異常信息的數據包(package)是Exception類、或其子類的一個對象。Ruby預定義了一個簡潔的異常層次結構,這個層次結構使得處理異常變得相當簡單。
當需要引發(raise)異常時, 可以使用某個內建的Exception類,或者創建自己異常類。如果創建自己的異常類,可能你希望它從StandardError類或其子類派生,否則,你的異常在默認情況下不會被捕獲。
每個Exception都關聯有一個消息字符串和?;厮菪畔ⅲ╞acktrace)。如果定義自己的異常,可以添加額外的信息。
8.2 處理異常(Handing Exception)
我們的點唱機用TCP套接字從互聯網下載歌曲。它的基本代碼很簡單(假設文件名個套接字都已經創建好)。
op_file = File.open(opfile_name, "w")
while data = socket.read(512)
op_file.write(data)
end
如果下載過程中得到一個致命錯誤,會發生什么呢?我們肯定不想在歌曲列表中存儲一首不完整的歌曲?!癐 Did it My...”
讓我們添加一些處理異常的代碼,看看它是如何幫助處理異常的。在一個beigin/end塊中,使用一個或多個rescue語句告訴Ruby希望處理的異常類型。
在這個特定的例子中,我們感興趣的是捕獲SystemCallError異常(同時暗含著任何SystemCallError子類的異常),所以它就是出現在resuce行的異常類型,在這個錯誤處理block中,我們報告了錯誤,關閉和刪除了輸出文件,同時重新引起異常。
op_file = File.open(opfile_name, "w")
begin
#這段代碼引發異常會被
#下面的rescue語句捕獲
while data = socket.read(512)
op_file.write(data)
end
rescue SystemCallError
$stderr.print "IO failed: " + $!
op_file.close
File.delete(opfile_name)
raise
end
當異常被引發時,Ruby將關于Exception對象的引用放在全局變量$!中,這與任何隨后的異常處理不相干。
Exception
fatal(used internally by Ruby)
NoMemoryError
-
ScriptError
- LoadError
- NotImplementError
- SyntaxError
-
SignalException
- Interrupt
-
StandardError
- ArgumentError
- IOError
- EOFError
- IndexError
- LocalJumpError
- NameError
- NoMethodError
- RangeError
- FloatDomainError
- RegexpError
- RuntimeError
- SecurityError
- SystemCallError
- system-dependent exception(Errno::XXX)
- ThreadError
- TypeError
- ZeroDivisionError
SystemExit
SystemStackError
這個感嘆號大概反映出了我們的驚訝,我們的代碼竟然會導致錯誤!在前面的例子中,我們用$!變量去格式化錯誤信息。
關閉和刪除文件后,我們可以不帶任何參數來調用raise,它會重新引導$!中的異常,這是一個有用的技術,它允許我們先編寫代碼過濾掉一些異常,再把不能處理的異常傳遞到更高的層次。這幾乎就像實現了一個錯誤處理的繼承層次結構。
在begin塊中可以有多個rescue子句(clause),每個rescue子句可以指示捕獲多個異常,在rescue子句的結束處,你可以提供一個Ruby的局部變量名來接收匹配的異常,許多人發現這比導出使用$!有更好的可讀性。
begin
eval string
rescue SyntaxError, NameError => boom
print "String doesn't compileL: " + boom
rescue Standard => bang
print "Error running script: " + bang
end
RUby如何決定執行哪個rescue子句呢?
這個處理非常類似于對case語句的處理,RUby用引發的異常依次比較begin塊中每個rescue子句的每個參數。如果引發的異常匹配了一個參數,Ruby就執行rescue的程序體,同時停止比較。
匹配是用parameter ===$!完成的。對于大多數異常來說,如果rescue子句給出的類型,與當前引發的異常的類型相同,或者它是引發異常的超類(superclass),這意味著匹配是成功的。如果編寫一個不帶參數表的rescue子句,它的默認參數是StandardError。
注意:可以這樣進行比較的原因是:異常是類,而類進而是某種Module。===方法是為模塊定義的,如果操作數的類與接收者相同或者接收者的祖先,這個方法返回true。
如果沒有任何rescue子句與之匹配,或者異常在begin/end塊外面被引發,Ruby就沿著調用棧向上查找,在調用者上尋找異常的處理者,接著在調用者的調用者上尋找,依次類推。
盡管,rescue子句的參數通常是Exception類的名稱,實際上它們可以是任何返回的Exception類的表達式(包括方法調用)。
8.2.1 系統錯誤(System Errors)
當對操作系統的調用返回錯誤碼時,會引發系統錯誤,在POSIX系統上,這些錯誤名稱有諸如EAGAIN和EPERM等(在Unix機器上,鍵入man error,你會得到這些錯誤的列表)。
Ruby得到這些錯誤,把每個錯誤裝(wrap)到特定的對象中,每個錯誤都是SystemCallError的子類,定義在Errno模塊中,這意味著,你會發現類名如Errno::EAGIN, ERRno::EIO和Errno::EPERM等的異常,如果想得到底層的系統錯誤碼,則把每個Errno異常對象有一個Errno(有點令人疑惑)的類常量(class constant),它包含相應的系統錯誤。
Errno::EAGAIN::Errno -> 35
Errno::EPERM::Errno -> 1
注意到EWOULDBLOCK和EAGAIN有相同的錯誤碼,這是我電腦上的操作系統的一個特性——兩個常量映射到相同的錯誤碼。為了處理這種情況,Ruby做出了安排,讓Errno::EAGAIN和Errno::EWOULDBOCK在rescue子句中被等同對待,如果你要求rescue其中一個錯誤,那么另一個也會被rescue。通過定義SystemCallError#===可以做到這點,因此,如果要比較SystemCallError的兩個子類,是比較它們的錯誤碼而不是它們在層次結構中的位置。
8..2.2 善后(Tidying Up)
有時候你需要寶恒一些處理在block結束時能夠被執行,而不管是否有異常引發。比如,也許在block的入口處(entry)打開一個文件,需要確保當block退出時它會被關閉。
ensure子句就是做這個的。ensure跟在最后的rescue子句后面,它包含一段block退出時總是被執行的代碼。不管block是否正常退出,是否引發并rescue異常,或者是否被捕獲的異常終止——這個ensure塊總會得到運行。
f = File.open("testfile")
begin
# .. process
rescue
# .. handle error
ensure
f.close unless f.nil?
end
盡管不是那么有用,else子句是一個類似ensure子句的構造,如果存在的話,它會出現在rescue子句之后和任何一個ensure子句之前。else子句的程序體,只有當主代碼體沒有引發任何異常時才會被執行。
f = File.open("testfile")
begin
# ..
rescue
# ..
else
# ..
puts "congratulations -- no errors!"
ensure
f.close unless f.nil
end
8.2.3 再次執行(Play It Again)
有時候也許可以糾正異常的原因。在這些例子中,你可以在rescue子句中使用retry語句去重復執行整個begin/end區塊。顯然這很可能導致無線循環,所以使用這個特性應該倍加小心(同時把一根手指輕輕放在鍵盤的中斷鍵上,隨時準備著)。
下面的例子再出現異常時會重新執行,它來自Minero Aoki的net/stmp.rb。
@esmtp = true
begin
#首先嘗試擴展登錄,如果因為服務器不支持而失敗
#則使用正常登陸
if @esmtp then
@command.ehlo(helodom)
else
@command.helo(helodom)
end
rescue ProtocolError
if @esmtp then
@esmtp = false
retry
else
raise
end
end
這段代碼首先使用EHLO命令試圖連接SMTP服務器,而這個命令并沒有被廣泛支持,如果連接嘗試失敗了,則設置@esmtp變量為false,同時重試連接。如果第二次連接也失敗了,則引發異常給它的調用者。
8.3 引發異常(Raising Exceptions)
到目前為止,我們一直都處于守勢,處理那些被別人引發的異常,該是輪到我們進攻的時候了(有些人說本書這些溫和的作者總是咄咄逼人,但那是另外一本書)。
可是使用Kernel.raise方法在代碼中引發異常(或者它有點判決意味的同義詞,Kernel.fail)
raise
raise "bad mp3 encoding"
raise InterfaceException, "Keyboard failure", caller
第一種形式只是簡單地重新引發當前異常(如果沒有當前異常的話,已發RuntimeError)。這種形式用于首先截獲異常再將其繼續傳遞的異常處理方法中。
第二種形式創建新的RuntimeError異常,把它的消息設置為指定的字符串,然后異常隨著調用棧向上引發。
第三種形式使用第一個參數創建異常,然后把相關聯的消息設置給第二個參數,同時把棧信息(trace)設置給第三個參數。通常,第一個參數是Exception層次結構中某個類的名稱,或者是某個異常類的對象實例的引用,通常使用Kernel.caller方法產生棧信息。
下面是使用raise的典型例子。
raise
raise "Missing name" if name.nil?
if i >= names.size
raise IndexError, "#{i} >= size (#{names.size})"
end
raise ArgumentError, "Name too big", caller
最后這個例子從?;厮菪畔h除當前函數,這在程序庫模塊中十分有用。可以更進一步:下面的代碼通過只將調用棧的子集傳遞給新異常,從而達到從?;厮菪畔⒅袆h除兩個函數的目的。
raise ArgumentError, "name too big", caller[1..-1]
8.3.1 添加信息到異常(Adding Information to Exceptions)
你可以定義自己的異常,保存任何需要從錯誤發生地傳遞出去的信息。
例如,取決于外部環境,某種類型的網絡錯誤可能是暫時的。如果這種錯誤發生了,而環境是適宜的,則可以在異常中設置一個標志,告訴異常處理程序重試這個操作可能是值得。
class RetryExceptipn < RuntimeError
attr :ok_to_retry
def initialize(ok_to_retry)
@ok_to_retry = ok_to_retry
end
end
注意:從技術層面講,這個參數可以是任何對象,只要它能響應消息Exception,且這個消息返回一個能夠滿足object.kind_of?(Exception)為真的對象。
在下面的代碼里面,發生了一個暫時的錯誤。
def read_data(socket)
data = socket.read(512)
if data.nil?
raise RetryException.new(true), "transient read error"
end
# .. 正常處理
end
在上一級的調用棧處理了異常。
begin
stuff = read_data(socket)
# .. process stuff
resuce RetryException => detail
retry of detail.ok_to_retry
raise
end
8.4 捕獲和拋出(Catch and Throw)
盡管raise和rescue的異常機制對程序出錯時終止執行已經夠用了,但是如果在正常處理過程期間能夠從一些深度嵌套的結構中跳轉出來,則是很棒的,catch和throw應運而生,可以方便地做到這點。
catch (:done) do
while line = gets
throw :done unless fields = line.split(/\t/)
songlist.add(Song.new(*fields))
end
songlist.play
end
catch定義了以給定名稱(可能符號或字符串)為標簽的block,這個block會正常執行直到暈倒throw為止。
當Ruby碰到throw,它迅速回溯(zip back)調用棧,用匹配的符號尋找catch代碼塊,當發現它之后,Ruby將棧清退(unwind)到這個為止并終止該block。所以,在前面的例子中,如果輸入沒有包含正確格式化的行,throw會跳到相應的catch代碼塊的結束處,不僅終止了while循環,而且跳出了歌曲列表的播放。
如果調用throw時制定了可選的第二個參數,這個值會作為catch的值返回。
在下面的例子中,如果響應任意提示符時鍵入!,使用throw終止與用戶的交互。
def prompt_and_get(prompt)
print prompt
res = readline.chomp
throw :quit_requested if res == "!"
res
end
catch :quit_requested do
name = prompt_and_get("Name: ")
age = prompt_and_get("Age:")
sex = prompt_and_get("Sex:")
end
這個例子說明了throw沒必要出現在catch的靜態作用域中。
第9章 Modules
模塊一種將方法、類與常量組織在一起的方式,模塊給你提供了兩個主要的好處:
1.模塊提供了命名空間(namespace)來防止命令沖突
2.模塊實現了mixin功能
9.1 命名空間(Namespace)
當你開始編寫越來越大的Ruby程序時,你自然會發現自己編寫了許多可重用的代碼——將先關的例程(routine)組成一個庫通常是合適的。你會希望將這些代碼分解不同的文件,使其內容可以被其他不同的Ruby程序共享。
通常代碼會被組織為類,你可能會讓一個類(或一組相關的類)對應一個文件。
不過,有時你想要把那些無法自然構成類的部分集合到一起。
一種初步的方法是將所有內容放到一個文件中,然后簡單地在任何需要它的程序中加載(load)它,這是C語言工作的方式,不過,這種方式有一個問題。假設你要編寫一組三角函數sin、cos等等,你將它們全部塞到一個文件trig.rb中,為后世享用。
同時,Sally想要模擬善良和邪惡,并且她編寫了一組對自己有用的例程,包括be_good和sin,并將它們放到moral.rb中,Joe想要編寫一個程序找出針尖上有多少跳舞的天使,想要在他的程序中加載。
答案是使用模塊機制,模塊定義了一個namespace(命名空間),它是一個沙箱(sandbox),你的方法和常量可以在其中任意發揮,而無需擔心被其他方法或常量干擾,三角函數可以放到一個模塊中。
module Trig
PI = 3.1415926
def Trig.sin(x)
end
def Trig.cos(x)
end
end
而品行好壞的方法可以放到另一個模塊中。
module Moral
VARY_BAD = 0
BAD = 1
def Moral.sin(badness)
end
end
模塊常量的命名和類常量一樣,都以大寫字母開頭,方法定義同樣看起來很相似:這些模塊方法就類似于類方法的定義。
如果第三方的程序想要使用這些模塊,它可以簡單地加載兩個文件(使用Ruby的require語句)并引用它們的完整名稱(qualified name)。
require 'trig'
require 'moral'
y = Trig.sin(Trig::PI/4)
wrongdoing = Moral.sin(Moral::VERY_BAD)
同類方法一樣,你可以用模塊名和句點來調用模塊方法, 使用模塊名和兩個冒號來引用常量。
9.2 Mixin
模塊有另一個妙用,它提供了一種稱為mixin的功能,以雷霆之勢,極大地的消除了對多重繼承的需要。
在上一節的示例中,我們定義了模塊方法,它們的名字都以模塊名為前綴,如果這讓你想到類方法,你接下來的想法可能是“如果我在模塊內定義實例方法會怎么樣?”
好問題,模塊并沒有實例,因為模塊并不是類,不過,你可以在類的定義中,include一個模塊,當包含發生時,模塊所有的實例方法瞬間在類中也可以使用了。它們被混入(mixin)了,實際上,所混入的模塊其實際行為就像是一個超類。
module Debug
def who_am_i?
"#{self.class.name} (\##{self.object_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.who_am_i?
et.who_am_i?
通過包含Debug模塊,Phonograph和EightTrack都得以訪問who_am_i?這個實例方法。
在繼續前行之前,我們將探討關于include語句的幾點問題,首先,include與文件無關,C程序員使用叫做#include的預處理器指令,在編譯期將一個文件的內容插入到另一個文件中,Ruby語句只是簡單地產生一個指向指定模塊的引用。如果模塊位于另一個文件中,在使用include之前,你必須使用require(或者不那么常用的旁系,load)將文件加載進來。第二點,Ruby的include并非簡單地將模塊的實例方法拷貝到類中,相反,它建立一個由類到所包含模塊的引用。
如果多個類包含這個模塊,它們都指向相同的內容,即使當程序正在運行時,如果你改變模塊中一個方法的定義,所有包含這個模塊的類都會表現出新的行為。
Mixin為你向類中添加功能,提供了一種控制精巧的方式,不過,它們真正的力量的,當mixin的代碼和使用它的類中的代碼開始交互時,它們會一起迸發出來,讓我們以標準的Ruby mixin——Comparable為例。
你可以使用Comparable mixin向類中添加比較操作符(<, <=, ==, >=和>)以及between?方法。為了使其能夠工作,Comparable假定任何使用它的類都定義了<=>操作符,這樣,作為類的一個編寫者,你定義一個方法<=>,再包含Comparable,然后就可以免費得到6個比較函數,讓我們用Song類來嘗試一下,令它們基于時長來進行比較,我們所要做的就是包含Comparable模塊并實現比較操作符<=>。
class Song
include Comparable
def initialize(name, artist, duration)
@name = name
@artist = artist
@duration = duration
end
def <=>(other)
self.duration <=> other.duration
end
end
我們可以用幾首測試歌曲來檢查一下結果。
song1 = Song.new("My Way", "Sinatra", 225)
song2 = Song.new("Bicyclops", "Fleck", 260)
song1 <=> song2
song2 < song2
song1 == song1
song1 > song2
9.3 迭代器與可枚舉模塊(lterators and the Enumerable Module)
你可能已經注意到Ruby收集(collection)類支持大量針對收集的各種操作:遍歷、排序等等,你可能會想,“哎呀,如果我自己的類也能支持這些出色的特性,那就太好了!”
當然,你的類可以支持所有這些出色的特性,感謝mixin和Enumerable模塊的魔力,你要做的就是編寫一個稱為each的迭代器,由它依次返回收集中的元素。包含Enumerable,然后你的類瞬間支持諸如map、include?和find_all等操作。
如果在你收集的對象中使用<=>方法是想了有意義的排序語義,你還會得到諸如min、max和sort等方法。
9.4 組合模塊(Composing Modules)
我們討論了Enumerable的inject方法,Enumerable是另一個標準的mixin,它基于宿主類(host class)中的each實現了許多方法,因此,我們可以在任何包括了Enumerable模塊并定義了each方法的類中使用了inject。許多內建的類都是如此。
[1, 2, 3, 4, 5 ].inject {|v, n| v+n }
('a'..'m').inject {|v,n| v+n}
我們還可以定義自己的類以包含Enumerable,繼而得到inject支持。
class VowelFinder
include Enumerable
def initialize(string)
@string = string
end
def each
@string.scan(/[aeiou]/) do |vowel|
yield vowel
end
end
end
vf = VowelFinder.new("the quick brown fox jumped")
vf.inject {|v,n| v+n}
注意,我們使用了和前面實例中調用inject的相同模式——使用它來求和,當作用于數字時,它返回算術和,當作用于字符串時,返回串聯的字符串。我們也可以使用一個模塊來封裝這個功能。
module Summable
def sum
inject {|v, n| v+n}
end
end
class Array
include Summable
end
class Range
include Summable
end
class VomelFinder
include Summable
end
[1, 2, 3, 4, 5].sum
vf = VowelFinder.new("the quick brown fox jumped")
vf.sum
9.4.1 Mixin中的實例變量(Instance variables in Mixins)
從C++轉向Ruby的人經常問我們,“mixin中的實例變量會如何呢?在C++中,我必須兜好幾個圈子才能控制如何在多重繼承中共享變量。Ruby是如何處理的呢?”
我們告訴他們,好吧,對初學者來說,這根本不是什么問題,回憶一下Ruby中實例變量是如何工作的:當前綴為@的變量第一次出現時,即在當前對象(也就是self)中創建實例變量。
對mixin來說,這意味著你要混入客戶類中的模塊,可能會在客戶對象中創建實例變量,并可能使用attr_reader或類似方法,定義這些實例變量的訪問方法。例如,下面實例中的Observable模塊,會向包含它類中添加實例變量@observer_list。
module Observable
def observers
@observer_list ||= []
end
def add_observer(obj)
observers << obj
end
def notify_observers
observers.each {|o| o.update}
end
end
多數時候,mixin模塊并不帶有它們自己的實例數據——它們只是使用訪問訪問方法從客戶對象中取得數據。但是,如果你要創建的mixin不得不持有它們自己的狀態。確保這個實例變量具有唯一的名字,可以對系統中其他的mixin區別開來(也許使用模塊名作為變量名的一部分)?;蛘?,模塊可以使用模塊一級的散列表,以當前對象的ID作為索引,來保存特定于實例的數據,而不必使用Ruby的實例變量。
module Test
State = {}
def state = (value)
State[object_id] = value
end
def state
State[object_id]
end
end
class Client
include Test
end
c1 = Client.new
9.4.2 解析有歧義的方法名(Resolving Ambiguous Method Names)
關于mixin,人們經常問到的另一個問題是,方法查找是如何處理的?特別的是,如果類、父類以及類所包含的mixin中,都定義有相同名字的方法時,會發生什么?
答案是,Ruby首先會從對象的直屬類(immediate class)中查找,然后是類所包含的mixin,之后是超類以及超類的mixin。如果一個類有多個混入的模塊,最后一個包含的模塊將會被第一個搜索。
9.5 包含其他文件(including other Files)
因為Ruby可以使我們輕松地編寫良好的、模塊化的代碼,你經常會發現自己編寫了含有大量自包含功能的小文件——比如x的接口、完成y的算法,等等。典型地,你會將這些文件組織為類或庫。
產生這些文件之后,你希望在新的程序中結合使用它們,Ruby有兩個語句來完成這一點,每次當load方法執行時,都會將制定的Ruby源文件包含進來。
load "filename.rb"
更常見的是使用require方法來加載指定的文件,且只加載一次。
require 'filename'
被加載文件中的局部變量不會蔓延到加載它們所在的范圍中。例如,這里有一個名為include.rb。
a = 1
def b
2
end
下面是當我們把它包含到另一個文件中時,將會發生什么?
a = "cat"
b = "dog"
require "included"
require有額外的功能:它可以加載共享的二進制庫,兩者都可以接受相對或者絕對路徑,如果指定了一個相對路徑(或者只是一個簡單的名字),它們將會在當前加載路徑中(load path——$:)的每個目錄下搜索這個文件。
使用load或者require所加載的文件,當然也可以包含其他文件,而這些文件又包含別的文件,依次類推,可能不那么明顯的是,require是一個可執行的語句——它可能在一個if語句內、或者可能包含一個剛剛拼合的字符串。搜索路徑也可以在運行時更改,只需將你希望的目錄加入$:數組中。
因為load會無條件地包含源文件,你可以使用它來重新加載一個在程序開始執行厚可能更改的源文件,下面是一個認為設計的示例。
這并不是嚴格為真的,Ruby在數組$中保存了被require所加載的文件列表,不過,這個列表只包括了調用require時所指定的文件名,欺騙Ruby讓它多次加載同一個文件,是有可能。
5.times do |i|
File.open("temp.rb", "w")
f.puts "module Temp"
f.puts " def Temp.var"
end
load "temp.rb"
puts Temp.var
end
對于這個功能,可以考慮一個不太實際的例子:Web應用重新加載正在運行的模塊,這讓它能動態地更新自己;它不需要重新啟動來集成軟件的新版本,這是使用例如Ruby等動態語言的眾多好處之一。