《Effective-Ruby》讀書筆記

本篇是在我接觸了 Ruby 很短一段時間後有幸捧起的一本書,下面結合本身的一些思考,來輸出一下本身的讀書筆記git

前言

學習一門新的編程語言一般須要通過兩個階段:程序員

  • 第一個階段是學習這門編程語言的語法和結構,若是咱們具備其餘編程語言的經驗,那麼這個過程一般只須要很短的時間;
  • 第二個階段是深刻語言、學習語言風格,許多編程語言在解決常見的問題時都會使用獨特的方法,Ruby 也不例外。

《Effictive Ruby》就是一本致力於讓你在第二階段更加深刻和全面的瞭解 Ruby,編寫出更具可讀性、可維護性代碼的書,下面我就着一些我認爲的重點和本身的思考來進行一些精簡和說明github

第一章:讓本身熟悉 Ruby

第 1 條:理解 Ruby 中的 True

  • 每一門語言對於布爾類型的值都有本身的處理方式,在 Ruby 中,除了 false 和 nil,其餘值都爲真值,包括數字 0 值。
  • 若是你須要區分 false 和 nil,可使用 nil? 的方式或 「==「 操做符並將 false 做爲左操做對象。
# 將 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

第 2 條:全部對象的值均可能爲 nil

在 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。緩存

第 3 條:避免使用 Ruby 中古怪的 Perl 風格語法

  • 推薦使用 String#match 替代 String#=~。前者將匹配信息以 MatchDate 對象返回,而非幾個特殊的全局變量。
  • 使用更長、更表意的全局變量的別名,而非其短的、古怪的名字(好比,用 $LOAD_PATH 替代 $: )。大多數長的名字須要在加載庫 English 以後才能使用。
  • 避免使用隱式讀寫全局變量 $_ 的方法(好比,Kernel#print、Regexp#~ 等)
# 這段代碼中有兩個 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

第 4 條:留神,常量是可變的

最開始接觸 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

第 5 條:留意運行時警告

  • 使用命令行選項 」-w「 來運行 Ruby 解釋器以啓用編譯時和運行時的警告。設置環境變量 RUBYOPT 爲 」-w「 也能夠達到相同目的。
  • 若是必須禁用運行時的警告,能夠臨時將全局變量 $VERBOSE 設置爲 nil。
# 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

第二章:類、對象和模塊

第 6 條:瞭解 Ruby 如何構建集成體系

讓咱們直接從代碼入手吧:

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 方法是如何使其複雜化的)

要點回顧:

  • 要尋找一個方法,Ruby 只須要向上搜索類體系。若是沒有找到這個方法,就從起點開始搜搜 method_missing 方法。
  • 包含模塊時 Ruby 會悄悄地建立單例類,並將其插入在繼承體系中包含它的類的上方。
  • 單例方法(類方法和針對對象的方法)存儲於單例類中,它也會被插入繼承體系中。

第 7 條:瞭解 super 的不一樣行爲

  • 當你想重載繼承體系中的一個方法時,關鍵字 super 能夠幫你調用它。
  • 不加括號地無參調用 super 等價於將宿主方法的素有參數傳遞給要調用的方法。
  • 若是但願使用 super 而且不向重載方法傳遞任何參數,必須使用空括號,即 super()。
  • 當 super 調用失敗時,自定義的 method_missing 方法將丟棄一些有用的信息。在第 30 條中有 method_missing 的替代解決方案。

第 8 條:初始化子類時調用 super

  • 當建立子類對象時,Ruby 不會自動調用超類中的 initialize 方法。做爲替代,常規的方法查詢規則也適用於 initialize 方法,只有第一個匹配的副本會被調用。
  • 當爲顯式使用繼承的類定義 initialize 方法時,使用 super 來初始化其父類。在定義 initialize_copy 方法時,應使用相同的規則
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

第 9 條:提防 Ruby 最棘手的解析

這是一條關於 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

第 10 條:推薦使用 Struct 而非 Hash 存儲結構化數據

看代碼吧:

# 假設你要對一個保存了年度天氣數據的 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

另外從其餘地方看到了關於 Struct::new 的實踐

  • 考慮使用 Struct.new, 它能夠定義一些瑣碎的 accessors, constructor(構造函數) 和 comparison(比較) 操做。
# 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
  • 考慮使用 Struct.new,它替你定義了那些瑣碎的存取器(accessors),構造器(constructor)以及比較操做符(comparison operators)。
# 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
  • 要去 extend 一個 Struct.new - 它已是一個新的 class。擴展它會產生一個多餘的 class 層級 而且可能會產生怪異的錯誤若是文件被加載屢次。

第 11 條:經過在模塊中嵌入代碼來建立命名空間

  • 經過在模塊中嵌入代碼來建立命名空間
  • 讓你的命名空間結構和目錄結構相同
  • 若是使用時可能出現歧義,可以使用 」::」 來限定頂級常量(好比,::Array)

第 12 條:理解等價的不一樣用法

看看下面的 IRB 回話而後自問一下:爲何方法 equal? 的返回值和操做符 「==」 的不一樣呢?

irb> "foo" == "foo"
---> true
irb> "foo".equal?("foo")
---> false

事實上,在 Ruby 中有四種方式來檢查對象之間的等價性,下面來簡單總個結吧:

  • 毫不要重載 equal? 方法。該方法的預期行爲是,嚴格比較兩個對象,僅當它們同時指向內存中同一對象時其值爲真(即,當它們具備相同的 object_id 時)
  • Hash 類在衝突檢查時使用 eql? 方法來比較鍵對象。默認實現可能和你的想像不一樣。遵循第 13 條建議以後再使用別名 eql? 來替代 「==」 書寫更合理的 hash 方法
  • 使用 「==」 操做符來測試兩個對象是否表示相同的值。有些類好比表示數字的類會有一個粗糙的等號操做符進行類型轉換
  • case 表達式使用 「===「 操做符來測試每一個 when 語句的值。左操做數是 when 的參數,右操做數是 case 的參數

第 13 條:經過 "<=>" 操做符實現比較和比較模塊

要記住在 Ruby 語言中,二元操做符最終會被轉換成方法調用的形式,左操做數對應着方法的接受者,右操做數對應着方法第一個也是惟一的那個參數。

  • 經過定義 "<=>" 操做符和引入 Comparable 模塊實現對象的排序
  • 若是左操做數不能與右操做數進行比較,"<=>" 操做符應該返回 nil
  • 若是要實現類的 "<=>" 運算符,應該考慮將 eql? 方法設置爲 "==" 操做符的別名,特別是當你但願該類的全部實例能夠被用來做爲哈希鍵的時候,就應該重載哈希方法

第 14 條:經過 protected 方法共享私有狀態

  • 經過 protected 方法共享私有狀態
  • 一個對象的 protected 方法若要被顯式接受者調用,除非該對象與接受者是同類對象或其具備相同的定義該 protected 方法的超類
# 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

第 15 條:優先使用實例變量而非類變量

  • 優先使用實例變量(@)而非類變量(@@)
  • 類也是對象,因此它們擁有本身的私有實例變量集合

第三章:集合

第 16 條:在改變做爲參數的集合以前複製它們

在 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"

第 17 條:使用 Array 方法將 nil 及標量對象轉換成數組

  • 使用 Array 方法將 nil 及標量對象轉換成數組
  • 不要將哈希傳給 Array 方法,它會被轉化成一個嵌套數組的集合
# 考慮下面這樣一個訂披薩的類:
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

第 18 條:考慮使用集合高效檢查元素的包含性

(書上對於這一條建議的描述足足有 4 頁半,但其實能夠看下面結論就ok,結尾有實例代碼)

  • 考慮使用 Set 來高效地檢測元素的包含性
  • 插入 Set 的對象必須也被當作哈希的鍵來用
  • 使用 Set 以前要引入它
# 原始版本
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

第 19 條:瞭解如何經過 reduce 方法摺疊集合

儘管可能有點雲裏霧裏,但仍是考慮考慮先食用代碼吧:

# 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

上面的代碼中:

  1. 首先,咱們使用了流行的 map 方法遍歷每一個元素,並將每一個元素 +1 處理,而後返回新的數組;
  2. 其次,咱們使用了 sort 方法對數組的元素進行排序,排序採用了 ASCII 字母排序
  3. 最後,咱們使用了查找方法 select 返回數組的第一個元素

reduce 方法到底幹了什麼?它爲何這麼特別?在函數式編程的範疇中,它是一個能夠將一個數據結構轉換成另外一種結構的摺疊函數。

讓咱們先從宏觀的角度來看摺疊函數,當使用如 reduce 這樣的摺疊函數時你須要瞭解以下三部分:

  • 枚舉的對象是 reduce 消息的接受者。某種程度上這是你想轉換的原始集合。顯然,它的類必須引入 Enumberable 模塊,不然你沒法對它調用 reduce 方法;
  • 塊會被源集合中的每一個元素調用一次,和 each 方法調用塊的方式相似。但和 each 不一樣的是,傳入 reduce 方法的塊必須產生一個返回值。這個返回值表明了經過當前元素最終摺疊生成的數據結構。咱們將會經過一些例子來鞏固這一知識點。
  • 一個表明了目標數據結構起始值的對象,被稱爲累加器。每一次塊的調用都會接受當前的累加器值並返回新的累加器值。在全部元素都被摺疊進累加器後,它的最終結構也就是 reduce 的返回值。

此時瞭解了這三部分你能夠回頭再去看一看代碼。

試着回想一下上一次使用 each 的場景,reduce 可以幫助你改善相似下面這樣的模式:

hash = {}
 
array.each do |element|
    hash[element] = true
end

第 20 條:考慮使用默認哈希值

我肯定你是一個曾經在塊的語法上徘徊許久的 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

因此看過上面的代碼框隱藏的內容後你會發現:

  1. 若是某段代碼在接受哈希的非法鍵時會返回 nil,不要爲傳入該方法的哈希使用默認值
  2. 相比使用默認值,有些時候用 Hash#fetch 方法能更加安全

第 21 條:對集合優先使用委託而非繼承

這一條也能夠被命名爲「對於核心類,優先使用委託而非繼承」,由於它一樣適用於 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

(更多的探索在書上.這裏只是簡單給一下結論.感興趣的童鞋再去看看吧!)

因此要點回顧一下:

  • 對集合優先使用委託而非繼承
  • 不要忘記編寫用來複制委託目標的 initialize_copy 方法
  • 編寫 freeze、taint 以及 untaint 方法時,先傳遞信息給委託目標,以後調用 super 方法。

第四章:異常

第 22 條:使用定製的異常而不是拋出字符串

  • 避免使用字符串做爲異常,它們會被轉換成原生的 RuntimeError 對象。取而代之,建立一個定製的異常類
  • 定製的異常類應該繼承自 StandardError,且類名應該以 "Error" 結尾
  • 當爲一個工程建立了不止一個異常類時,從建立一個繼承自 StandardError 的基類開始。其餘的異常類應該繼承自該定製的基類
  • 若是你對你的定製異常類編寫了 initialize 方法,務必確保其調用了 super 方法,最好在調用時以錯誤信息做爲參數
  • 在 initialize 方法中設置錯誤信息時,請牢記:若是在 raise 方法中再度設置錯誤信息會覆蓋本來在 initialize 中設置的那一條
class TemperatureError < StandardError
    attr_reader(:temperature)
 
    def initialize(temperature)
        @temperature = temperature
        super("invalid temperature: #@temperature")
    end
end

第 23 條:捕獲可能的最具體的異常

  • 只捕獲那些你知道如何恢復的異常
  • 當捕獲異常時,首先處理最特殊的類型。在異常的繼承關係中位置越高的,越應該排在 rescue 鏈的後面
  • 避免捕獲如 StandardError 這樣的通用異常。若是你已經這麼作了,就應該想一想你真正想作的是否是能夠經過 ensure 語句來實現
  • 在異常發生的狀況下,從 resuce 語句中拋出的異常將會替換當前異常並離開當前的做用域

第 24 條:經過塊和 ensure 管理資源

  • 經過 ensure 語句來釋聽任何已得到的資源
  • 經過在類方法上使用塊和 ensure 語句將資源管理的邏輯抽離出來
  • 確保 ensure 語句中使用的變量已經被初始化過了

第 25 條:經過臨近的 end 退出 ensure 語句

  • 避免在 ensure 語句中顯式使用 return 語句,這意味着方法體內存在着某些錯誤的邏輯
  • 一樣,不要在 ensure 語句中直接使用 throw,你應該將 throw 放在方法主體內
  • 當執行迭代時,不要在 ensure 語句中執行 next 或 break。仔細想一想在迭代內到底需不須要 begin 塊。將關係反轉或許更加合理,就是將迭代放在 begin 塊中
  • 通常來講,不要再 ensure 語句中改變控制流,在 rescue 語句中完成這樣的工做,你的意圖會更加清晰

第 26 條:限制 retry 次數,改變重試頻率並記錄異常信息

  • 永遠不要無條件 retry,要把它看作代碼中的隱式循環;在代碼塊的外圍定義重試次數,當超出最大重試次數時從新拋出異常
  • retry 時記錄具備審計做用的異常信息,若是重試有問題的代碼解決不了問題,須要追根溯源地去了解異常是如何發生的
  • 當在 retry 以前使用延時時,須要考慮增長延時避免加重問題

第 27 條:throw 比 raise 更適合用來跳出做用域

  • 在複雜的流程控制中,能夠考慮使用 throw 和 raise,這種方法一個額外的好處是能夠把一個對象傳遞到上層調用棧並做爲 catch 的最終返回值
  • 儘可能使用簡單的方法來控制程序結果,能夠經過方法調用和 return 重寫 catch 和 throw

第五章:元編程

第 28 條:熟悉 Ruby 模塊和類的鉤子方法

  • 全部的鉤子方法都須要被定義爲單例方法
  • 添加、刪除、取消定義方法的鉤子方法參數是方法名,而不是類名,若是須要,使用 self 去獲取類的信息
  • 定義 singleton_method_added 會出發自身
  • 不要覆蓋 extend_object、append_features 和 prepend_features 方法,使用 extended、included 和 prepended 替代

第 29 條:在類的鉤子方法中執行 super 方法

  • 在類的鉤子方法中執行 super 方法

第 30 條:推薦使用 define_method 而非 method_missing

  • define_method 優於 method_missing
  • 若是必須使用 method_missing,最好也定義 respond_to_missing? 方法

第 31 條:瞭解不一樣類型的 eval 間的差別

  • 使用 instance_eval 和 instance_exec 定義的單例方法
  • class_eval、module_eval、class_exec 和 module_exec 方法只能夠被模塊或者方法使用。經過這些定義的方法都是實例方法

第 32 條:慎用猴子補丁

  • 儘管 refinement 已經再也不是實驗性的功能,它仍然有可能被修改得更加成熟
  • 在不一樣的語法做用域,在使用 refinement 以前必須先激活它

第 33 條:使用別名鏈執行被修改的方法

  • 在設置別名鏈時,須要確保別名是獨一無二的
  • 必要的時候要考慮提供一個撤銷別名鏈的方法

第 34 條:支持多種 Proc 參數數量

  • 與弱 Proc 對象不一樣,在參數數量不匹配時,強 Proc 對象會拋出 ArgumentError 異常
  • 可使用 Proc#arity 方法獲得 Proc 指望的參數數量,若是返回的是正數,則意味着有多少參數是必須的。若是返回的是負數,則意味着 Proc 有些參數是可選的,能夠經過 "~" 來獲得有多少是必須參數

第 35 條:使用模塊前置時請謹慎思考

  • prepend 方法在使用時對類體系機構的影響是:它將模塊插入到接受者以前。這和 include 方法有很大不一樣:include 則是將模塊插入到接受者和其超類之間
  • 與 included 和 extended 模塊鉤子同樣,前置模塊也會出發 prepended 鉤子

第六章:測試

第 36 條:熟悉單元測試工具 MiniTest

  • 測試方法須要以 "test_" 做爲前綴
  • 簡短的測試更容易理解,也更容易維護
  • 使用合適的斷言方法生成更易讀的出錯信息
  • 斷言(Assertion)和反演(refutation)的文檔在 MiniTest::Assertions 中

第 37 條:熟悉 MiniTest 的需求測試

  • 使用 describe 方法建立測試類,使用 it 定義測試用例
  • 雖然在需求說明測試中,斷言仍然可用,可是更推薦使用注入到 Object 中的指望方法
  • 在 MiniTest::Expectations 模塊中,能夠找到關於指望方法更詳細的文檔

第 38 條:使用 Mock 模擬特定對象

  • 使用 Mock 來隔離外部系統的不穩定因素
  • Mock 或者替換沒有被測試過得方法,有可能會讓這些被 Mock 的代碼在生產環境中出現問題
  • 請確保在測試方法代碼的最後調用了 MiniTest::Mock#verity 方法

第 39 條:力爭代碼被有效測試過

  • 使用模糊測試和屬性測試工具,幫助測試代碼的快樂路徑和異常路徑。
  • 測試覆蓋率工具會給你一種虛假的安全感,由於被執行過的代碼不表明這行代碼是正確的
  • 在編寫特性的同時就加上測試,會讓測試容易得多
  • 在你開始尋找致使 bug 的根本緣由以前,先寫一個針對該 bug 的測試
    儘量多地自動化你的測試

第七章:工具與庫

第 40 條:學會使用 Ruby 文檔

  • ri 工具用來讀取文檔,rdoc 工具用來生成文檔
  • 使用命令行選項 "-d doc" 來爲 RI 工具制定在 "doc" 路徑下查找文檔
    運行 rdoc 時,後面跟上命令行選項 "-f ri" 來爲 RI 工具生成文檔。另外,用 "-f darkfish" 來生成 HTML 格式的文檔(本身測試過..對於大型項目生成的 HTML 文檔不是很友好..)
  • 完整的 RDoc 文檔能夠在 RDoc::Markup 類中找到(使用 RI 查閱)

第 41 條:認識 IRB 的高級特性

  • 在 IRB::ExtendCommandBundle 模塊,或者一個會被引入 IRB::ExtendCommandBundle 中的模塊中自定義 IRB 命令
  • 利用下劃線變量("")來獲取上一個表達式的結果(例如,last_elem =
  • irb 命令能夠用來建立一個新的會話,並將當前的評估上下文改變成任意對象
    考慮 Pry gem 做爲 IRB 的替代品

第 42 條:用 Bundler 管理 Gem 依賴

  • 在加載完 Bundler 以後,使用 Bundler.require 會犧牲一點點靈活性,可是能夠加載 Gemfile 中全部的 gem
  • 當開發應用時,在 Gemfile 中列出全部的 gem,而後把 Gemfile.lock 添加到版本控制系統中
  • 當打包 RubyGem,在 gem 規格文件中列出 gem 全部依賴,但不要把 Gemfile.lock 添加到你的版本系統中

第 43 條:爲 Gem 依賴設定版本上限

  • 忽略掉版本上限需求至關於你說了你能夠支持將來全部的版本
  • 相對於悲觀版本操做符,更加傾向於使用明確的版本範圍
  • 當公佈發佈一個 gem 時,指明依賴包的版本限制要求,在安全的範圍內越寬越好,上限能夠擴展到下一個主要發佈版本以前

第八章:內存管理與性能

第 44 條:熟悉 Ruby 的垃圾收集器

擴展閱讀:
Ruby GC 自述 · Ruby China 
Ruby 2.1:RGenGC

垃圾收集器是個複雜的軟件工程。從很高的層次看,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

第 45 條:用 Finalizer 構建資源安全網

  • 最好使用 ensure 子句來保護有限的資源。
  • 若是必需要在 ensure 子句外報錄一個資源(resource),那麼就給它建立一個 finalizer(終結方法)
  • 永遠不要再這樣一個綁定中建立 finalizer Proc,該綁定引用了一個註定會被銷燬的對象,這會形成垃圾收集器沒法釋放該對象
  • 記住,finalizer 可能在一個對象銷燬後以及程序終止前的任什麼時候間被調用

第 46 條:認識 Ruby 性能分析工具

  • 在修改性能差的代碼以前,先使用性能分析工具收集性能相關的信息。
  • 在 ruby-prof gem 和 Ruby 自帶的標準 profile 庫之間,選擇前者,由於前者更快並且能夠提供多種不一樣的報告。
  • 若是使用 Ruby 2.1 或者更新的版本,應該考慮使用 stackprof gem 和 memory_profiler gem。

第 47 條:避免在循環中使用對象字面量

  • 將循環中的不會變化的對象字面量變成常量。
  • 在 Ruby 2.1 及更高的版本中凍結字符串字面量,至關於把它做爲常量,能夠被整個運行程序共享。

第 48 條:考慮記憶化大開銷計算

  • 考慮提供一個方法經過將緩存的變量職位 nil 來重置記憶化。
  • 確保時鐘認真考慮過這些由記憶化而跳過反作用所致使的後果。
  • 若是不但願調用者修改緩存的變量,那應該考慮讓被記憶化的方法返回凍結對象。
  • 先用工具分析程序的性能,再考慮是否須要記憶化。

總結

週末學習了兩天才勉強看完了一遍,對於 Ruby 語言的有一些高級特性仍是比較吃力的,須要本身反反覆覆的看才能理解一二。不過好在也是有收穫吧,沒有白費本身的努力,特意總結一個精簡版方便後面的童鞋學習。

另外這篇文章最開始是使用公司的文檔空間建立的,發現 Markdown 雖然精簡易於使用,可是功能性上比一些成熟的寫文工具要差上不少,就好比對代碼的支持吧,用公司的代碼塊還支持自定義標題、顯示行號、是否能縮放、主題等一系列自定義的東西,寫出來的東西也更加友好...


按照慣例黏一個尾巴:

歡迎轉載,轉載請註明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz 歡迎關注公衆微信號:wmyskxz 分享本身的學習 & 學習資料 & 生活 想要交流的朋友也能夠加qq羣:3382693

相關文章
相關標籤/搜索