本篇是在我接觸了 Ruby 很短一段時間後有幸捧起的一本書,下面結合本身的一些思考,來輸出一下本身的讀書筆記git
學習一門新的編程語言一般須要通過兩個階段:程序員
《Effictive Ruby》就是一本致力於讓你在第二階段更加深刻和全面的瞭解 Ruby,編寫出更具可讀性、可維護性代碼的書,下面我就着一些我認爲的重點和本身的思考來進行一些精簡和說明github
# 將 false 放在左邊意味着 Ruby 會將表達式解析爲 FalseClass#== 方法的調用(該方法繼承自 Object 類) # 這樣咱們能夠很放心地知道:若是右邊的操做對象也是 false 對象,那麼返回值爲 true if false == x ... end # 換句話說,把 false 置爲有操做對象是有風險的,可能不一樣於咱們的指望,由於其餘類可能覆蓋 Object#== 方法從而改變下面這個比較 class Bad def == (other) true end end irb> false == Bad.new ---> false irb> Bad.new == false ---> true
在 Ruby 中倡導接口高於類型,也就是說預期要求對象是某個給定類的實例,不如將注意力放在該對象能作什麼上。沒有什麼會阻止你意外地把 Time 類型對象傳遞給接受 Date 對象的方法,這些類型的問題雖然能夠經過測試避免,但仍然有一些多態替換的問題使這些通過測試的應用程序出現問題:數據庫
undefined method 'fubar' for nil:NilClass (NoMethodError)
當你調用一個對象的方法而其返回值恰好是討厭的 nil 對象時,這種狀況就會發生···nil 是類 NilClass 的惟一對象。這樣的錯誤會悄然逃過測試而僅在生產環境下出現:若是一個用戶作了些超乎尋常的事。編程
另外一種致使該結果的狀況是,當一個方法返回 nil 並將其做爲參數直接傳給一個方法時。事實上存在數量驚人的方式能夠將 nil 意外地引入你運行中的程序。最好的防範方式是:假設任何對象均可覺得 nil,包括方法參數和調用方法的返回值。c#
# 最簡單的方式是使用 nil? 方法 # 若是方法接受者(receiver)是 nil,該方法將返回真值,不然返回假值。 # 如下幾行代碼是等價的: person.save if person person.save if !person.nil? person.save unless person.nil? # 將變量顯式轉換成指望的類型經常比時刻擔憂其爲 nil 要容易得多 # 尤爲是在一個方法即便是部分輸入爲 nil 時也應該產生結果的時候 # Object 類定義了幾種轉換方法,它們能在這種狀況下派上用場 # 好比,to_s 方法會將方法接受者轉化爲 string: irb> 13.to_s ---> "13" irb> nil.to_s ---> "" # to_s 如此之棒的緣由在於 String#to_s 方法只是簡單返回 self 而不作任何轉換和複製 # 若是一個變量是 string,那麼調用 to_s 的開銷最小 # 但若是變量期待 string 而剛好獲得 nil,to_s 也能幫你扭轉局面: def fix_title (title) title.to_s.capitalize end
這裏還有一些適用於 nil 的最有用的例子:api
irb> nil.to_a ---> [] irb> nil.to_i ---> 0 irb> nil.to_f ---> 0.0
當須要同時考慮多個值的時候,你可使用類 Array 提供的優雅的討巧方式。Array#compact 方法返回去掉全部 nil 元素的方法接受者的副本。這在將一組可能爲 nil 的變量組裝成 string 時很經常使用。好比:若是一我的的名字由 first、middle 和 last 組成(其中任何一個均可能爲 nil),那麼你能夠用下面的代碼組成這個名字:數組
name = [first, middle, last].compact.join(" ")
nil 對象的嗜好是在你不經意間偷偷溜進正在運行的程序中。不管它來自用戶輸入、無約束數據庫,仍是用 nil 來表示失敗的方法,意味着每一個變量均可能爲 nil。緩存
$LOAD_PATH
替代 $:
)。大多數長的名字須要在加載庫 English 以後才能使用。# 這段代碼中有兩個 Perl 語法。 # 第一個:使用 String#=~ 方法 # 第二個:在上述代碼中看起來好像是使用了一個全局變量 $1 導出第一個匹配組的內容,但其實不是... def extract_error (message) if message =~ /^ERROR:\s+(.+)$/ $1 else "no error" end end # 如下是替代方法: def extract_error (message) if m = message.match(/^ERROR:\s+(.+)$/) m[1] else "no error" end end
最開始接觸 Ruby 時,對於常量的認識大概可能就是由大寫字母加下劃線組成的標識符,例如 STDIN、RUBY_VERSION。不過這並非故事的所有,事實上,由大寫字母開頭的任何標識符都是常量,包括 String 或 Array,來看看這個:安全
module Defaults NOTWORKS = ["192.168.1","192.168.2"] end def purge_unreachable (networks=Defaults::NETWORKS) networks.delete_if do |net| !ping(net + ".1") end end
若是調用方法 unreadchable 時沒有加參數的話,會意外的改變一個常量的值。在 Ruby 中這樣作甚至都不會警告你。好在有一種解決這個問題的方法——freeze 方法:
module Defaults NOTWORKS = ["192.168.1","192.168.2"].freeze end
加入你再想改變常量 NETWORKS 的值,purge_unreadchable 方法就會引入 RuntimeError 異常。根據通常的經驗,老是經過凍結常量來阻止其被改變,然而不幸的是,凍結 NETWORKS 數組還不夠,來看看這個:
def host_addresses (host, networks=Defaults::NETWORKS) networks.map {|net| net << ".#{host}"} end
若是第二個參數沒有賦值,那麼 host_addresses 方法會修改數組 NETWORKS 的元素。即便數組 NETWORKS 自身被凍結,可是元素仍然是可變的,你可能沒法從數組中增刪元素,但你必定能夠對存在的元素加以修改。所以,若是一個常量引用了一個集合,好比數組或者是散列,那麼請凍結這個集合以及其中的元素:
module Defaults NETWORKS = [ "192.168.1", "192.168.2" ].map(&:freeze).freeze end
甚至,要達到防止常量被從新賦值的目的,咱們能夠凍結定義它的那個模塊:
module Defaults TIMEOUT = 5 end Defaults.freeze
# test.rb def add (x, y) z = 1 x + y end puts add 1, 2 # 使用不帶 -w 參數的命令行 irb> ruby test.rb ---> 3 # 使用帶 -w 參數的命令行 irb< ruby -w test.rb ---> test.rb:1: warning: parentheses after method name is interpreted as an argument list, not a decomposed argument ---> test.rb:2: warning: assigned but unused variable - z ---> 3
讓咱們直接從代碼入手吧:
class Person def name ... end end class Customer < Person ... end irb> customer = Customer.new ---> #<Customer> irb> customer.superclass ---> Person irb> customer.respond_to?(:name) ---> true
上面的代碼幾乎就和你預想的那樣,當調用 customer 對象的 name 方法時,Customer 類會首先檢查自身是否有這個實例方法,沒有那麼就繼續搜索。
順着集成體系向上找到了 Person 類,在該類中找到了該方法並將其執行。(若是 Person 類中沒有找到的話,Ruby 會繼續向上直到到達 BasicObject)
可是若是方法在查找過程當中直到類樹的根節點仍然沒有找到匹配的辦法,那麼它將從新從起點開始查找,不過這一次會查找 method_missing 方法。
下面咱們開始讓事情變得更加有趣一點:
module ThingsWithNames def name ... end end class Person include(ThingsWithNames) end irb> Person.superclass ---> Object irb> customer = Customer.new ---> #<Customer> irb> customer.respond_to?(:name) ---> true
這裏把 name 方法從 Person 類中取出並移到一個模塊中,而後把模塊引入到了 Person 類。Customer 類的實例仍然能夠如你所料響應 name 方法,可是爲何呢?顯然,模塊 ThingsWithNames 並不在集成體系中,由於 Person 類的超類仍然是 Object 類,那會是什麼呢?其實,Ruby 在這裏對你撒謊了!當你 include 方法來將模塊引入類時,Ruby 在幕後悄悄地作了一些事情。它建立了一個單例類並將它插入類體系中。這個匿名的不可見類被鏈向這個模塊,所以它們共享了實力方法和常量。
當每一個模塊被類包含時,它會當即被插入集成體系中包含它的類的上方,之後進先出(LIFO)的方式。每一個對象都經過變量 superclass 連接,像單鏈表同樣。這惟一的結果就是,當 Ruby 尋找一個方法時,它將以逆序訪問訪問每一個模塊,最後包含的模塊最早訪問到。很重要的一點是,模塊永遠不會重載類中的方法,由於模塊插入的位置是包含它的類的上方,而 Ruby 老是會在向上檢查以前先檢查類自己。
(好吧······這不是所有的事實。確保你閱讀了第 35 條,來看看 Ruby 2.0 中的 prepend 方法是如何使其複雜化的)
要點回顧:
class Parent def initialize (name) @name = name end end class Child < Parent def initialize (grade) @grade = grade end end # 你能看到上面的窘境,Ruby 沒有提供給子類和其超類的 initialize 方法創建聯繫的方式 # 咱們可使用通用意義上的 super 關鍵字來完成繼承體系中位於高層的辦法: class Child < Parent def initialize (name, grade) super(name) # Initialize Parent. @grade = grade end end
這是一條關於 Ruby 可能會戲弄你的另外一條提醒,要點在於:Ruby 在對變量賦值和對 setter 方法調用時的解析是有區別的!直接看代碼吧:
# 這裏把 initialize 方法體中的內容當作第 counter= 方法的調用也不是毫無道理 # 事實上 initialize 方法會建立一個新的局部變量 counter,並將其賦值爲 0 # 這是由於 Ruby 在調用 setter 方法時要求存在一個顯式接受者 class Counter attr_accessor(:counter) def initialize counter = 0 end ... end # 你須要使用 self 充當這個接受者 class Counter attr_accessor(:counter) def initialize self.counter = 0 end ... end # 而在你調用非 setter 方法時,不須要顯式指定接受者 # 換句話說,不要使用沒必要要的 self,那會弄亂你的代碼: class Name attr_accessor(:first, :last) def initialize (first, last) self.first = first self.last = last end def full self.first + " " + self.last # 這裏沒有調用 setter 方法使用 self 多餘了 end end # 就像上面 full 方法裏的註釋,應該把方法體內的內容改成 first + " " + last
看代碼吧:
# 假設你要對一個保存了年度天氣數據的 CSV 文件進行解析並存儲 # 在 initialize 方法後,你會得到一個固定格式的哈希數組,可是存在如下的問題: # 1.不能經過 getter 方法訪問其屬性,也不該該將這個哈希數組經過公共接口向外暴露,由於其中包含了實現細節 # 2.每次你想在類內部使用該哈希時,你不得不回頭來看 initialize 方法 # 由於你不知道CSV具體的對應是怎樣的,並且當類成熟狀況可能還會發生變化 require('csv') class AnnualWeather def initialize (file_name) @readings = [] CSV.foreach(file_name, headers: true) do |row| @readings << { :date => Date.parse(row[2]), :high => row[10].to_f, :low => row[11].to_f, } end end end # 使用 Struct::new 方法的返回值賦給一個常量並利用它建立對象的實踐: class AnnualWeather # Create a new struct to hold reading data. Reading = Struct.new(:date, :high, :low) def initialize (file_name) @readings = [] CSV.foreach(file_name, headers: true) do |row| @readings << Reading.new(Date.parse(row[2]), row[10].to_f, row[11].to_f) end end end # Struct 類自己比你第一次使用時更增強大。除了屬性列表,Struct::new 方法還能接受一個可選的塊 # 也就是說,咱們能在塊中定義實例方法和類方法。好比,咱們定義一個返回平均每個月平均溫度的 mean 方法: Reading = Struct.new(:date, :high, :low) do def mean (high + low) / 2.0 end end
# good class Person attr_reader :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end end # better class Person < Struct.new(:first_name, :last_name) end
# good class Person attr_accessor :first_name, :last_name def initialize(first_name, last_name) @first_name = first_name @last_name = last_name end end # better Person = Struct.new(:first_name, :last_name) do end
看看下面的 IRB 回話而後自問一下:爲何方法 equal? 的返回值和操做符 「==」 的不一樣呢?
irb> "foo" == "foo" ---> true irb> "foo".equal?("foo") ---> false
事實上,在 Ruby 中有四種方式來檢查對象之間的等價性,下面來簡單總個結吧:
要記住在 Ruby 語言中,二元操做符最終會被轉換成方法調用的形式,左操做數對應着方法的接受者,右操做數對應着方法第一個也是惟一的那個參數。
# Ruby 語言中,私有方法的行爲和其餘面向對象的編程語言中不太相同。Ruby 語言僅僅在私有方法上加了一條限制————它們不能被顯式接受者調用 # 不管你在繼承關係中的哪一級,只要你沒有使用接受者,你均可以調用祖先方法中的私有方法,可是你不能調用另外一個對象的私有方法 # 考慮下面的例子: # 方法 Widget#overlapping? 會檢測其自己是否和另外一個對象在屏幕上重合 # Widget 類的公共接口並無將屏幕座標對外暴露,它們的具體實現都隱藏在了內部 class Widget def overlapping? (other) x1, y1 = @screen_x, @screen_y x2, y2 = other.instance_eval {[@screen_x, @screen_y]} ... end end # 能夠定義一個暴露私有屏幕座標的方法,但並不經過公共接口來實現,其實現方式是聲明該方法爲 protected # 這樣咱們既保持了原有的封裝性,也使得 overlapping? 方法能夠訪問其自身以及其餘傳入的 widget 實例的座標 # 這正式設計 protected 方法的緣由————在相關類之間共享私有信息 class Widget def overlapping? (other) x1, y1 = @screen_x, @screen_y x2, y2 = other.screen_coordinates ... end protected def screen_coordinates [@screen_x, @screen_y] end end
在 Ruby 中多數對象都是經過引用而不是經過實際值來傳遞的,當將這種類型的對象插入容器時,集合類實際存儲着該對象的引用而不是對象自己。
(值得注意的是,這條準則是個例如:Fixnum 類的對象在傳遞時老是經過值而不是引用傳遞)
這也就意味着當你把集合做爲參數傳入某個方法並進行修改時,原始集合也會所以被修改,有點間接,不過很容易看到這種狀況的發生。
Ruby 語言自帶了兩個用來複制對象的方法:dup 和 clone。
它們都會基於接收者建立新的對象,可是與 dup 方法不一樣的是,clone 方法會保留原始對象的兩個附加特性。
首先,clone 方法會保留接受者的凍結狀態。若是原始對象的狀態是凍結的,那麼生成的副本也會是凍結的。而 dup 方法就不一樣了,它永遠不會返回凍結的對象。
其次,若是接受這種存在單例方法,使用 clone 也會複製單例類。因爲 dup 方法不會這樣作,因此當使用 dup 方法時,原始對象和使用 dup 方法建立的副本對於相同消息的響應多是不一樣的。
# 也可使用 Marshal 類將一個集合及其所持有的元素序列化,而後再反序列化: irb> a = ["Monkey", "Brains"] irb> b = Marshal.load(Marshal.dump(a)) irb> b.each(&:upcasel); b.first ---> "MONKEY" irb> a.last ---> "Brains"
# 考慮下面這樣一個訂披薩的類: class Pizza def initialize (toppings) toppings.each do |topping| add_and_price_topping(topping) end end end # 上面的 initialize 方法期待的是一個 toppings 數組,但咱們能傳入單個 topping,甚至是在沒有 topping 對象的時候直接傳入 nil # 你可能會想到使用可變長度參數列表來實現它,並將參數類型改成 *topping,這樣會把全部的參數整合成一個數組。 # 儘管這樣作可讓咱們傳入單個 topping 對象,擔當傳入一組對象給 initialize 方法的時候必須使用 "*" 顯式將其拓展成一個數組。 # 因此這樣作僅僅是拆東牆補西牆罷了,一個更好的解決方式是將傳入的參數轉換成一個數組,這樣咱們就明確地知道我要作的是什麼了 # 先對 Array() 作一些探索: irb> Array('Betelgeuse') ---> ["Betelgeuse"] irb> Array(nil) ---> [] irb> Array(['Nadroj', 'Retep']) ---> ["Nadroj", "Retep"] irb> h = {pepperoni: 20,jalapenos: 2} irb> Array(h) ---> [[:pepperoni, 20], [:jalapenos, 2]] # 若是你想處理一組哈希最好採用第 10 條的建議那樣 # 回答訂披薩的問題上: # 通過一番改造,它如今可以接受 topping 數組、單個 topping,或者沒有 topping(nil or []) class Pizza def initialize (toppings) Array(toppings).each do |topping| add_and_price_topping(topping) end end ... end
(書上對於這一條建議的描述足足有 4 頁半,但其實能夠看下面結論就ok,結尾有實例代碼)
# 原始版本 class Role def initialize (name, permissions) @name, @permissions = name, permissions end def can? (permission) @permissions.include?(permission) end end # 版本1.0:使用 Hash 替代 Array 的 Role 類: # 這樣作基於兩處權衡,首先,由於哈希只存儲的鍵,因此數組中的任何重複在轉換成哈希的過程當中都會丟失。 # 其次,爲了可以將數組轉換成哈希,須要將整個數組映射,構建出一個更大的數組,從而轉化爲哈希。這將性能問題從 can? 方法轉移到了 initialize 方法 class Role def initialize (name, permissions) @name = name @permissions = Hash[permissions.map {|p| [p, ture]}] end def can? (permission) @permissions.include?(permission) end end # 版本2.0:引入 Set: # 性能幾乎和上一個哈希版本的同樣 require('set') class Role def initialize (name, permissions) @name, @permissions = name, Set.new(permissions) end def can? (permission) @permissions.include?(permission) end end # 最終的例子 # 這個版本自動保證了集合中沒有重複的記錄,且重複條目是很快就能被檢測到的 require('set') require('csv') class AnnualWeather Reading = Struct.new(:date, :high, :low) do def eql? (other) date.eql?(other.date); end def hash; date.hash; end end def initialize (file_name) @readings = Set.new CSV.foreach(file_name, headers: true) do |row| @readings << Reading.new(Date.parse(row[2]), row[10].to_f, row[11].to_f) end end end
儘管可能有點雲裏霧裏,但仍是考慮考慮先食用代碼吧:
# reduce 方法的參數是累加器的起始值,塊的目的是建立並返回一個適用於下一次塊迭代的累加器 # 若是原始集合爲空,那麼塊永遠也不會被執行,reduce 方法僅僅是簡單地返回累加器的初始值 # 要注意塊並無作任何賦值。這是由於在每一個迭代後,reduce 丟棄上次迭代的累加器並保留了塊的返回值做爲新的累加器 def sum (enum) enum.reduce(0) do |accumulator, element| accumulator + element end end # 另外一個快捷操做方式對處理塊自己很方便:能夠給 reduce 傳遞一個符號(symbol)而不是塊。 # 每一個迭代 reduce 都使用符號做爲消息名稱發送消息給累加器,同時將當前元素做爲參數 def sum (enum) enum.reduce(0, :+) end # 考慮一下把一個數組的值所有轉換爲哈希的鍵,而它們的值都是 true 的狀況: Hash[array.map {|x| [x, true]}] # reduce 可能會提供更加完美的方案(注意此時 reduce 的起始值爲一個空的哈希): array.reduce({}) do |hash, element| hash.update(element => true) end # 再考慮一個場景:咱們須要從一個存儲用戶的數組中篩選出那些年齡大於或等於 21 歲的人羣,以後咱們但願將這個用戶數組轉換成一個姓名數組 # 在沒有 reduce 的時候,你可能會這樣寫: users.select {|u| u.age >= 21}.map(&:name) # 上面這樣作固然能夠,但並不高效,緣由在於咱們使用上面的語句時對數組進行了屢次遍歷 # 第一次是經過 select 篩選出了年齡大於或等於 21 歲的人,第二次則還須要映射成只包含名字的新數組 # 若是咱們使用 reduce 則無需建立或遍歷多個數組: users.reduce([]) do |names, user| names << user.name if user.age >= 21 names end
引入 Enumerable 模塊的類會獲得不少有用的實例方法,它們可用於對對象的集合進行過濾、遍歷和轉化。其中最爲經常使用的應該是 map 和 select 方法,這些方法是如此強大以致於在幾乎全部的 Ruby 程序中你都能見到它們的影子。
像數組和哈希這樣的集合類幾乎已是每一個 Ruby 程序不可或缺的了,若是你還不熟悉 Enumberable 模塊中定義的方法,你可能已經本身寫了至關多的 Enumberable 模塊已經具有的方法,知識你還不知道而已。
Enumberable 模塊
戳開 Array 的源碼你能看到 include Enumberable 的字樣(引入的類必須實現 each 方法否則報錯),咱們來簡單闡述一下 Enumberable API:
irb> [1, 2, 3].map {|n| n + 1} ---> [2, 3, 4] irb> %w[a l p h a b e t].sort ---> ["a", "a", "b", "e", "h", "l", "p", "t"] irb> [21, 42, 84].first ---> 21上面的代碼中:
- 首先,咱們使用了流行的 map 方法遍歷每一個元素,並將每一個元素 +1 處理,而後返回新的數組;
- 其次,咱們使用了 sort 方法對數組的元素進行排序,排序採用了 ASCII 字母排序
- 最後,咱們使用了查找方法 select 返回數組的第一個元素
reduce 方法到底幹了什麼?它爲何這麼特別?在函數式編程的範疇中,它是一個能夠將一個數據結構轉換成另外一種結構的摺疊函數。
讓咱們先從宏觀的角度來看摺疊函數,當使用如 reduce 這樣的摺疊函數時你須要瞭解以下三部分:
此時瞭解了這三部分你能夠回頭再去看一看代碼。
試着回想一下上一次使用 each 的場景,reduce 可以幫助你改善相似下面這樣的模式:
hash = {} array.each do |element| hash[element] = true end
我肯定你是一個曾經在塊的語法上徘徊許久的 Ruby 程序員,那麼請告訴我,下面這樣的模式在代碼中出現的頻率是多少?
def frequency (array) array.reduce({}) do |hash, element| hash[element] ||= 0 # Make sure the key exists. hash[element] += 1 # Then increment it. hash # Return the hash to reduce. end end
這裏特意使用了 "||=" 操做符以確保在修改哈希的值時它是被賦過值的。這樣作的目的其實也就是確保哈希能有一個默認值,咱們能夠有更好的替代方案:
def frequency (array) array.reduce(Hash.new(0)) do |hash, element| hash[element] += 1 # Then increment it. hash # Return the hash to reduce. end end
看上去還真是那麼一回事兒,可是當心,這裏埋藏着一個隱蔽的關於哈希的陷阱。
# 先來看一下這個 IRB 會話: irb> h = Hash.new(42) irb> h[:missing_key] ---> 4二、 irb> h.keys # Hash is still empty! ---> [] irb> h[:missing_key] += 1 ---> 43 irb> h.keys # Ah, there you are. ---> [:missing_key] # 注意,當訪問不存在的鍵時會返回默認值,但這不會修改哈希對象。 # 使用 "+=" 操做符的確會像你想象中那般更新哈希,但並不明確,回顧一下 "+=" 操做符會展開成什麼可能會頗有幫助: # Short version: hash[key] += 1 # Expands to: hash[key] = hash[key] + 1 # 如今賦值的過程就很明確了,先取得默認值再進行 +1 的操做,最終將其返回的結果以一樣的鍵名存入哈希 # 咱們並無以任何方式改變默認值,固然,上面一段代碼的默認值是數字類型,它是不能修改的 # 可是若是咱們使用一個能夠修改的值做爲默認值並在以後使用了它狀況將會變得更加有趣: irb> h = Hash.new([]) irb> h[:missing_key] ---> [] irb> h[:missing_key] << "Hey there!" ---> ["Hey there!"] irb> h.keys # Wait for it... ---> [] irb> h[:missing_key] ---> ["Hey there!"] # 看到上面關於 "<<" 的小騙局了嗎?我從沒有改變哈希對象,當我插入一個元素以後,哈希並麼有改變,可是默認值改變了 # 這也是 keys 方法提示這個哈希是空可是訪問不存在的鍵時卻反悔了最近修改的值的緣由 # 若是你真想插入一個元素並設置一個鍵,你須要更深刻的研究,但另外一個不明顯的反作用正等着你: irb> h = Hash.new([]) irb> h[:weekdays] = h[:weekdays] << "Monday" irb> h[:months] = h[:months] << "Januray" irb> h.keys ---> [:weekdays, :months] irb> h[:weekdays] ---> ["Monday", "January"] irb> h.default ---> ["Monday", "Januray"] # 兩個鍵共享了同一個默認數組,多數狀況你並不想這麼作 # 咱們真正想要的是當咱們訪問不存在的鍵時能返回一個全新的數組 # 若是給 Hash::new 一個塊,當須要默認值時這個塊就會被調用,並友好地返回一個新建立的數組: irb> h = Hash.new{[]} irb> h[:weekdays] = h[:weekdays] << "Monday" ---> ["Monday"] irb> h[:months] = h[:months] << "Januray" ---> ["Januray"] irb> h[:weekdays] ---> ["Monday"] # 這樣好多了,但咱們還能夠往前一步。 # 傳給 Hash::new 的塊能夠有選擇地接受兩個參數:哈希自己和將要訪問的鍵 # 這意味着咱們若是想去改變哈希也是可的,那麼當訪問一個不存在的鍵時,爲何不將其對應的值設置爲一個新的空數組呢? irb> h = Hash.new{|hash, key| hash[key] = []} irb> h[:weekdays] << "Monday" irb> h[:holidays] ---> [] irb> h.keys ---> [:weekdays, :holidays] # 你可能發現上面這樣的技巧存在着重要的不足:每當訪問不存在的鍵時,塊不只會在哈希中建立新實體,同時還會建立一個新的數組 # 重申一遍:訪問一個不存在的鍵會將這個鍵存入哈希,這暴露了默認值存在的通用問題: # 正確的檢查一個哈希是否包含某個鍵的方式是使用 hash_key? 方法或使用它的別名,可是深感內疚的是一般狀況下默認值是 nil: if hash[key] ... end # 若是一個哈希的默認值不是 nil 或者 false,這個條件判斷會一直成功:將哈希的默認值設置成非 nil 可能會使程序變得不安全 # 另外還要提醒的是:經過獲取其值來檢查哈希某個鍵存在與否是草率的,其結果也可能和你所預期的不一樣 # 另外一種處理默認值的方式,某些時候也是最好的方式,就是使用 Hash#fetch 方法 # 該方法的第一個參數是你但願從哈希中查找的鍵,可是 fetch 方法能夠接受一個可選的第二個參數 # 若是指定的 key 在當前的哈希中找不到,那麼取而代之,fetch 的第二個參數會返回 # 若是你省略了第二個參數,在你試圖獲取一個哈希中不存在的鍵時,fetch 方法會拋出一個異常 # 相比於對整個哈希設置默認值,這種方式更加安全 irb> h = {} irb> h[:weekdays] = h.fetch(:weekdays, []) << "Monday" ---> ["Monday"] irb> h.fetch(:missing_key) keyErro: key not found: :missing_key
因此看過上面的代碼框隱藏的內容後你會發現:
這一條也能夠被命名爲「對於核心類,優先使用委託而非繼承」,由於它一樣適用於 Ruby 的全部核心類。
Ruby 的全部核心類都是經過 C語言 來實現的,指出這點是由於某些類的實例方法並無考慮到子類,好比 Array#reverse 方法,它會返回一個新的數組而不是改變接受者。
猜猜若是你繼承了 Array 類並調用了子類的 reverse 方法後會發生什麼?
# 是的,LikeArray#reverse 返回了 Array 實例而不是 LikeArray 實例 # 但你不該該去責備 Array 類,在文檔中有寫的很明白會返回一個新的實例,因此達不到你的預期是很天然的 irb> class LikeArray < Array; end irb> x = LikeArray.new([1, 2, 3]) ---> [1, 2, 3] irb> y = x.reverse ---> [3, 2, 1] irb> y.class ---> Array
固然還不止這些,集合上的許多其餘實例方法也是這樣,集成比較操做符就更糟糕了。
好比,它們容許子類的實例和父類的實例相比較,這說得通嘛?
irb> LikeArray.new([1, 2, 3]) == [1, 2, 3,] ---> true
繼承並非 Ruby 的最佳選擇,從核心的集合類中繼承更是毫無道理的,替代方法就是使用「委託」。
讓咱們來編寫一個基於哈希但有一個重要不一樣的類,這個類在訪問不存在的鍵時會拋出一個異常。
實現它有不少不一樣的方式,但編寫一個新類讓咱們能夠簡單的重用同一個實現。
與繼承 Hash 類後爲保證正確而處處修修補補不一樣,咱們這一次採用委託。咱們只須要一個實例變量 @hash,它會替咱們幹全部的重活:
# 在 Ruby 中實現委託的方式有不少,Forwardable 模塊讓使用委託的過程很是容易 # 它將一個存有要代理的方法的鏈表綁定到一個實例變量上,它是標準庫的一部分(不是核心庫),這也是須要顯式引入的緣由 require('forwardable') class RaisingHash extend(Forwardable) include(Enumerbale) def_delegators(:@hash, :[], :[]=, :delete, :each, :keys, :values, :length, :empty?, :hash_key?) end
(更多的探索在書上.這裏只是簡單給一下結論.感興趣的童鞋再去看看吧!)
因此要點回顧一下:
class TemperatureError < StandardError attr_reader(:temperature) def initialize(temperature) @temperature = temperature super("invalid temperature: #@temperature") end end
垃圾收集器是個複雜的軟件工程。從很高的層次看,Ruby 垃圾收集器使用一種被稱爲 標記-清除(mark and sweep)的過程。(熟悉 Java 的童鞋應該會感到一絲熟悉)
首先,遍歷對象圖,能被訪問到的對象會被標記爲存活的。接着,任何未在第一階段標記過的對象會被視爲垃圾並被清楚,以後將內存釋放回 Ruby 或操做系統。
遍歷整個對象圖並標記可訪問對象的開銷太大。Ruby 2.1 經過新的分代式垃圾收集器對性能進行了優化。對象被分爲兩類,年輕代和年老代。
分代式垃圾收集器基於一個前提:大多數對象的生存時間都不會很長。若是咱們知道了一個對象能夠存活好久,那麼就能夠優化標記階段,自動將這些老的對象標記爲可訪問,而不須要遍歷整個對象圖。
若是年輕代對象在第一階段的標記中存活了下來,那麼 Ruby 的分代式垃圾收集器就把它們提高爲年老代。也就是說,他們依然是可訪問的。
在年輕代對象和年老代對象的概念下,標記階段能夠分爲兩種模式:主要標記階段(major)和次要標記階段(minor)。
在主要標記階段,全部的對象(不管新老)都會被標記。該模式下,垃圾收集器不區分新老兩代,因此開銷很大。
次要標記階段,僅僅考慮年輕代對象,並自動標記年老代對象,而不檢查可否被訪問。這意味着年老代對象只會在主要標記階段以後纔會被清除。除非達到了一些閾值,保證整個過程所有做爲主要標記以外,垃圾收集器傾向於使用次要標記。
垃圾收集器的清除階段也有優化機制,分爲兩種模式:即便模式和懶惰模式。
在即便模式中,垃圾收集器會清除全部的未標記的對象。若是有不少對象須要被釋放,那這種模式開銷就很大。
所以,清除階段還支持懶惰模式,它將嘗試釋放盡量少的對象。
每當 Ruby 中建立一個新對象時,它可能嘗試觸發一次懶惰清除階段,去釋放一些空間。爲了更好的理解這一點,咱們須要看看垃圾收集器如何管理存儲對象的內存。(簡單歸納:垃圾收集器經過維護一個由頁組成的堆來管理內存。頁又由槽組成。每一個槽存儲一個對象。)
咱們打開一個新的 IRB 會話,運行以下命令:
`IRB``> ``GC``.stat` `---> {``:count``=>``9``, ``:heap_length``=>``126``, ...}`
GC::stat 方法會返回一個散列,包含垃圾收集器相關的全部信息。請記住,該散列中的鍵以及它們對應垃圾收集器的意義可能在下一個版本發生變化。
好了,讓咱們來看一些有趣的鍵:
鍵名 | 說明 |
---|---|
count | 垃圾收集器運行的總次數 |
major_gc_count | 主要模式下的運行次數 |
minor_gc_count | 次要模式下的運行次數 |
total_allocated_object | 程序開始時分配的對象總數 |
total_freed_object | Ruby 釋放的對象總數。與上面之差表示存活對象的數量,這能夠經過 heap_live_slot 鍵來計算 |
heap_length | 當前堆中的頁數 |
heap_live_slot 和 heap_free_slot | 表示所有頁中被使用的槽數和未被使用的槽數 |
old_object | 年老代的對象數量,在次要標記階段不會被處理。年輕代的對象數量能夠用 heap_live_slot 減去 old_object 來得到 |
該散列中還有幾個有趣的數字,但在介紹以前,讓咱們來學習垃圾收集器的最後一個要點。還記得對象是存在槽中的吧。Ruby 2.1 的槽大小爲 40 字節,然而並非全部的對象都是這麼大。
好比,一個包含 255 個字節的字符串對象。若是對象的大小超過了槽的大小,Ruby 就會額外向操做系統申請一塊內存。
當對象被銷燬,槽被釋放後,Ruby 會把多餘的內存還給操做系統。如今讓咱們看看 GC::stat 散列中的這些鍵:
鍵名 | 說明 |
---|---|
malloc_increase | 全部超過槽大小的對象所佔用的總比特數 |
malloc_limit | 閾值。若是 malloc_increase 的大小超過了 malloc_limit,垃圾收集器就會在次要模式下運行。一個 Ruby 應用程序的生命週期裏,malloc_limit 是被動調整的。它的大小是當前 malloc_increase 的大小乘以調節因子,這個因子默認是 1.4。你能夠經過環境變量 RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR 來設定這個因子 |
oldmalloc_increase 和 oldmalloc_limit | 是上面兩個對應的年老代值。若是 oldmalloc_increase 的大小超過了 oldmalloc_limit,垃圾收集器就會在主要模式下運行。oldmalloc_limit 的調節因子more是 1.2。經過環境變量 RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR 能夠設定它 |
做爲最後一部分,讓咱們來看針對特定應用程序進行垃圾收集器調優的環境變量。
在下一個版本的 Ruby 中,GC::stat 散列中的值對應的環境變量可能會發生變化。好消息是 Ruby 2.2 將支持 3 個分代,Ruby 2.1 只支持兩個。這可能會影響到上述變量的設定。
有關垃圾收集器調優的環境變量的權威信息保存在 "gc.c" 文件中,是 Ruby 源程序的一部分。
下面是 Ruby 2.1 中用於調優的環境變量(僅供參考):
環境變量名 | 說明 |
---|---|
RUBY_GC_HEAP_INIT_SLOTS | 初始槽的數量。默認爲 10k,增長它的值可讓你的應用程序啓動時減小垃圾收集器的工做效率 |
RUBY_GC_HEAP_FREE_SLOTS | 垃圾收集器運行後,空槽數量的最小值。若是空槽的數量小於這個值,那麼 Ruby 會申請額外的頁,並放入堆中。默認值是 4096 |
RUBY_GC_HEAP_GROWTH_FACTOR | 當須要額外的槽時,用於計算須要增長的頁數的乘數因子。用已使用的頁數乘以這個因子算出還須要增長的頁數、默認值是 1.8 |
RUBY_GC_HEAP_GROWTH_MAX_SLOTS | 一次添加到堆中的最大槽數。默認值是0,表示沒有限制。 |
RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR | 用於計算出發主要模式垃圾收集器的門限值的乘數因子。門限由前一次主要清除後年老代對象數量乘以該因子獲得。該門限與當前年老代對象數量成比例。默認值是 2.0。這意味着若是年老代對象在上次主要標記階段事後的數量翻倍的話,新一輪的主要標記過程將被出發。 |
RUBY_GC_MALLOC_LIMIT | GC::stat 散列中 malloc_limit 的最小值。若是 malloc_increase 超過了 malloc_limit 的值,那麼次要模式垃圾收集器就會運行一次。該設定用於確保 malloc_increase 不會小於特定值。它的默認值是 16 777 216(16MB) |
RUBY_GC_MALOC_LIMIT_MAX | 與 RUBY_GC_MALLOC_LIMIT 相反的值,這個設定保證 malloc_limit 不會變得過高。它能夠被設置成 0 來取消上限。默認值是 33 554 432(32MB) |
RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR | 控制 malloc_limit 如何增加的乘數因子。新的 malloc_limit 值由當前 malloc_limit 值乘以這個因子來得到,默認值爲 1.4 |
RUBY_GC_OLDMALLOC_LIMIT | 年老代對應的 RUBY_GC_MALLOC_LIMIT 值。默認值是 16 777 216(16MB) |
RUBY_GC_OLDMALLOC_LIMIT_MAX | 年老代對應的 RUBY_GC_MALLOC_LIMIT_MAX 值。默認值是 134 217 728(128MB) |
RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR | 年老代對應的 RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR 值。默認值是 1.2 |
週末學習了兩天才勉強看完了一遍,對於 Ruby 語言的有一些高級特性仍是比較吃力的,須要本身反反覆覆的看才能理解一二。不過好在也是有收穫吧,沒有白費本身的努力,特意總結一個精簡版方便後面的童鞋學習。
另外這篇文章最開始是使用公司的文檔空間建立的,發現 Markdown 雖然精簡易於使用,可是功能性上比一些成熟的寫文工具要差上不少,就好比對代碼的支持吧,用公司的代碼塊還支持自定義標題、顯示行號、是否能縮放、主題等一系列自定義的東西,寫出來的東西也更加友好...
按照慣例黏一個尾巴:
歡迎轉載,轉載請註明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz 歡迎關注公衆微信號:wmyskxz 分享本身的學習 & 學習資料 & 生活 想要交流的朋友也能夠加qq羣:3382693