更多資訊請至 rubyonrails.org:

Active Support 核心擴展

Active Support 是 Ruby on Rails 的組件,負責提供 Ruby 語言擴展和公用程式。

它在語言層級提供了更豐富的底層基礎,目標是開發 Rails 應用程式以及開發 Ruby on Rails 本身。

閱讀本指南後,您將了解

  • 核心擴展是什麼。
  • 如何載入所有擴展。
  • 如何精選您想要的擴展。
  • Active Support 提供哪些擴展。

1 如何載入核心擴展

1.1 獨立的 Active Support

為了盡可能縮小預設佔用空間,Active Support 預設會載入最少的相依性。它被分解成小塊,以便僅載入所需的擴展。它還具有一些方便的進入點,可以一次載入相關的擴展,甚至是所有擴展。

因此,在簡單的 require 之後,例如

require "active_support"

僅會載入 Active Support 框架所需的擴展。

1.1.1 精選一個定義

此範例示範如何載入 Hash#with_indifferent_access。此擴展可將 Hash 轉換為 ActiveSupport::HashWithIndifferentAccess,允許以字串或符號的方式存取鍵。

{ a: 1 }.with_indifferent_access["a"] # => 1

對於定義為核心擴展的每個方法,本指南都會有一條註解,說明該方法的定義位置。在 with_indifferent_access 的情況下,註解寫著

這表示您可以這樣 require 它

require "active_support"
require "active_support/core_ext/hash/indifferent_access"

Active Support 已經過仔細修訂,以便精選檔案僅會載入嚴格需要的相依性(如果有的話)。

1.1.2 載入分組的核心擴展

下一層是簡單地載入 Hash 的所有擴展。根據經驗法則,載入 active_support/core_ext/some_class 可以一次取得 SomeClass 的擴展。

因此,要載入 Hash 的所有擴展 (包括 with_indifferent_access)

require "active_support"
require "active_support/core_ext/hash"

1.1.3 載入所有核心擴展

您可能只想載入所有核心擴展,有一個檔案可以做到這一點

require "active_support"
require "active_support/core_ext"

1.1.4 載入所有 Active Support

最後,如果您想要所有可用的 Active Support,只需發出

require "active_support/all"

實際上,這甚至不會預先將整個 Active Support 載入記憶體中,某些東西會透過 autoload 進行設定,因此只有在使用時才會載入。

1.2 Ruby on Rails 應用程式中的 Active Support

除非 config.active_support.bare 為 true,否則 Ruby on Rails 應用程式會載入所有 Active Support。在這種情況下,應用程式只會載入框架本身為其自身需求精選的內容,並且仍然可以按照上一節所述的任何粒度層級自行精選。

2 所有物件的擴展

2.1 blank?present?

以下值在 Rails 應用程式中被視為空白

  • nilfalse

  • 僅由空格組成的字串 (請參閱下面的註解),

  • 空的陣列和雜湊,以及

  • 任何其他回應 empty? 且為空之物件。

字串的述詞使用 Unicode 感知的字元類別 [:space:],因此例如 U+2029(段落分隔符)被視為空格。

請注意,沒有提及數字。特別是,0 和 0.0 不是空白。

例如,來自 ActionController::HttpAuthentication::Token::ControllerMethods 的這個方法使用 blank? 來檢查是否存在權杖

def authenticate(controller, &login_procedure)
  token, options = token_and_options(controller.request)
  unless token.blank?
    login_procedure.call(token, options)
  end
end

present? 方法等同於 !blank?。此範例取自 ActionDispatch::Http::Cache::Response

def set_conditional_cache_control!
  unless self["Cache-Control"].present?
    # ...
  end
end

2.2 presence

如果 presence 方法的接收者是 present?,則會傳回接收者,否則傳回 nil。它對於像這樣的慣用語很有用

host = config[:host].presence || "localhost"

2.3 duplicable?

從 Ruby 2.5 開始,大多數物件都可以透過 dupclone 複製

"foo".dup           # => "foo"
"".dup              # => ""
Rational(1).dup     # => (1/1)
Complex(0).dup      # => (0+0i)
1.method(:+).dup    # => TypeError (allocator undefined for Method)

Active Support 提供 duplicable? 來查詢物件是否可複製

"foo".duplicable?           # => true
"".duplicable?              # => true
Rational(1).duplicable?     # => true
Complex(1).duplicable?      # => true
1.method(:+).duplicable?    # => false

任何類別都可以透過移除 dupclone 或從它們引發例外來禁止複製。因此,只有 rescue 可以判斷給定的任意物件是否可複製。duplicable? 取決於上述硬編碼列表,但它比 rescue 快得多。僅當您知道硬編碼列表在您的用例中足夠時才使用它。

2.4 deep_dup

deep_dup 方法會傳回給定物件的深層複製。通常,當您 dup 包含其他物件的物件時,Ruby 不會 dup 它們,因此它會建立物件的淺層複製。例如,如果您有一個包含字串的陣列,它會像這樣

array     = ["string"]
duplicate = array.dup

duplicate.push "another-string"

# the object was duplicated, so the element was added only to the duplicate
array     # => ["string"]
duplicate # => ["string", "another-string"]

duplicate.first.gsub!("string", "foo")

# first element was not duplicated, it will be changed in both arrays
array     # => ["foo"]
duplicate # => ["foo, "another-string"]

如您所見,在複製 Array 實例後,我們得到了另一個物件,因此我們可以修改它,而原始物件會保持不變。但是,對於陣列的元素而言,情況並非如此。由於 dup 不會進行深層複製,因此陣列內的字串仍然是同一個物件。

如果需要物件的深層複製,應該使用 deep_dup。以下範例說明:

array     = ["string"]
duplicate = array.deep_dup

duplicate.first.gsub!("string", "foo")

array     # => ["string"]
duplicate # => ["foo"]

如果物件不可複製,deep_dup 會直接回傳該物件。

number = 1
duplicate = number.deep_dup
number.object_id == duplicate.object_id   # => true

2.5 try

當您只想在物件不是 nil 的情況下呼叫方法時,最簡單的方式是使用條件陳述式,但這樣會增加不必要的混亂。替代方案是使用 trytry 類似於 Object#public_send,但若傳送給 nil 則會回傳 nil

以下範例說明:

# without try
unless @number.nil?
  @number.next
end

# with try
@number.try(:next)

另一個範例是來自 ActiveRecord::ConnectionAdapters::AbstractAdapter 的程式碼,其中 @logger 可能為 nil。您可以看到程式碼使用 try 並避免了不必要的檢查。

def log_info(sql, name, ms)
  if @logger.try(:debug?)
    name = "%s (%.1fms)" % [name || "SQL", ms]
    @logger.debug(format_log_entry(name, sql.squeeze(" ")))
  end
end

try 也可以不帶參數但帶區塊呼叫,此區塊只有在物件不是 nil 時才會執行。

@person.try { |p| "#{p.first_name} #{p.last_name}" }

請注意,try 會吞噬無此方法錯誤,改為回傳 nil。如果您想防止輸入錯誤,請改用 try!

@number.try(:nest)  # => nil
@number.try!(:nest) # NoMethodError: undefined method `nest' for 1:Integer

2.6 class_eval(*args, &block)

您可以使用 class_eval 在任何物件的單例類別的上下文中評估程式碼。

class Proc
  def bind(object)
    block, time = self, Time.current
    object.class_eval do
      method_name = "__bind_#{time.to_i}_#{time.usec}"
      define_method(method_name, &block)
      method = instance_method(method_name)
      remove_method(method_name)
      method
    end.bind(object)
  end
end

2.7 acts_like?(duck)

方法 acts_like? 提供了一種方法來檢查某些類別是否基於簡單的慣例,如同某些其他類別一樣運作:一個提供與 String 相同介面的類別定義

def acts_like_string?
end

這只是一個標記,其主體或回傳值並不重要。然後,用戶端程式碼可以透過這種方式查詢鴨子類型安全性

some_klass.acts_like?(:string)

Rails 有一些類別與 DateTime 類似,並遵循此合約。

2.8 to_param

Rails 中的所有物件都會回應方法 to_param,其目的是回傳一些可以代表它們作為查詢字串中的值,或作為 URL 片段的值。

預設情況下,to_param 只是呼叫 to_s

7.to_param # => "7"

to_param 的回傳值**不應**被轉義。

"Tom & Jerry".to_param # => "Tom & Jerry"

Rails 中的幾個類別會覆寫此方法。

例如,niltruefalse 會回傳它們本身。Array#to_param 會在元素上呼叫 to_param,並用 "/" 連接結果。

[0, true, String].to_param # => "0/true/String"

值得注意的是,Rails 路由系統會在模型上呼叫 to_param,以取得 :id 佔位符的值。ActiveRecord::Base#to_param 會回傳模型的 id,但您可以在模型中重新定義該方法。例如,給定

class User
  def to_param
    "#{id}-#{name.parameterize}"
  end
end

我們得到

user_path(@user) # => "/users/357-john-smith"

控制器需要注意 to_param 的任何重新定義,因為當像這樣的請求進入時,"357-john-smith" 是 params[:id] 的值。

2.9 to_query

to_query 方法會建構一個查詢字串,將指定的 keyto_param 的回傳值關聯。例如,使用以下 to_param 定義

class User
  def to_param
    "#{id}-#{name.parameterize}"
  end
end

我們得到

current_user.to_query("user") # => "user=357-john-smith"

此方法會轉義所需的任何內容,包括 key 和 value

account.to_query("company[name]")
# => "company%5Bname%5D=Johnson+%26+Johnson"

因此其輸出可以立即用於查詢字串中。

陣列會回傳將 to_query 應用於每個元素(以 key[] 作為 key)的結果,並用 "&" 連接結果。

[3.4, -45.6].to_query("sample")
# => "sample%5B%5D=3.4&sample%5B%5D=-45.6"

雜湊也會回應 to_query,但使用不同的簽名。如果沒有傳遞任何引數,則呼叫會產生一系列排序的 key/value 賦值,在其值上呼叫 to_query(key)。然後,它會用 "&" 連接結果。

{ c: 3, b: 2, a: 1 }.to_query # => "a=1&b=2&c=3"

方法 Hash#to_query 接受 key 的可選命名空間

{ id: 89, name: "John Smith" }.to_query("user")
# => "user%5Bid%5D=89&user%5Bname%5D=John+Smith"

2.10 with_options

方法 with_options 提供了一種方法來分解一系列方法呼叫中的通用選項。

給定預設選項雜湊,with_options 會產生一個代理物件到區塊。在區塊中,在代理物件上呼叫的方法會與它們的選項合併後轉送到接收器。例如,您可以擺脫以下的重複

class Account < ApplicationRecord
  has_many :customers, dependent: :destroy
  has_many :products,  dependent: :destroy
  has_many :invoices,  dependent: :destroy
  has_many :expenses,  dependent: :destroy
end

透過這種方式

class Account < ApplicationRecord
  with_options dependent: :destroy do |assoc|
    assoc.has_many :customers
    assoc.has_many :products
    assoc.has_many :invoices
    assoc.has_many :expenses
  end
end

該慣用語也可以向讀者傳達分組。例如,假設您想要發送一份語言取決於使用者的電子報。在郵件程式中的某處,您可以像這樣將與地區相關的位元分組

I18n.with_options locale: user.locale, scope: "newsletter" do |i18n|
  subject i18n.t :subject
  body    i18n.t :body, user_name: user.name
end

由於 with_options 會將呼叫轉送到其接收器,因此它們可以巢狀結構。每個巢狀層級除了自己的預設值外,還會合併繼承的預設值。

2.11 JSON 支援

相較於 json gem 通常為 Ruby 物件提供的實作,Active Support 提供了更好的 to_json 實作。這是因為某些類別,例如 HashProcess::Status 需要特殊處理才能提供適當的 JSON 表示法。

2.12 實例變數

Active Support 提供了幾個方法來簡化對實例變數的存取。

2.12.1 instance_values

方法 instance_values 會回傳一個雜湊,將不帶 "@" 的實例變數名稱對應到它們對應的值。Key 是字串

class C
  def initialize(x, y)
    @x, @y = x, y
  end
end

C.new(0, 1).instance_values # => {"x" => 0, "y" => 1}

2.12.2 instance_variable_names

方法 instance_variable_names 會回傳一個陣列。每個名稱都包含 "@" 符號。

class C
  def initialize(x, y)
    @x, @y = x, y
  end
end

C.new(0, 1).instance_variable_names # => ["@x", "@y"]

2.13 靜音警告和例外

方法 silence_warningsenable_warnings 會在區塊持續期間相應地變更 $VERBOSE 的值,然後再重設它。

silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger }

也可以使用 suppress 來靜音例外。此方法會接收任意數量的例外類別。如果在執行區塊期間引發的例外是 kind_of? 任何引數,則 suppress 會捕獲它並靜默回傳。否則,不會捕獲例外。

# If the user is locked, the increment is lost, no big deal.
suppress(ActiveRecord::StaleObjectError) do
  current_user.increment! :visits
end

2.14 in?

述詞 in? 會測試物件是否包含在另一個物件中。如果傳遞的引數未回應 include?,則會引發 ArgumentError 例外。

in? 的範例

1.in?([1, 2])        # => true
"lo".in?("hello")   # => true
25.in?(30..50)      # => false
1.in?(1)            # => ArgumentError

3 Module 的擴充

3.1 屬性

3.1.1 alias_attribute

模型屬性具有讀取器、寫入器和述詞。您可以透過使用 alias_attribute 為模型屬性建立別名,並為您定義所有對應的三個方法。如同其他別名方法一樣,新名稱是第一個引數,舊名稱是第二個 (一個助記符號是它們的順序與您執行賦值時相同)。

class User < ApplicationRecord
  # You can refer to the email column as "login".
  # This can be meaningful for authentication code.
  alias_attribute :login, :email
end

3.1.2 內部屬性

當您在打算被子類化的類別中定義屬性時,名稱衝突是一種風險。這對函式庫來說非常重要。

Active Support 定義了巨集 attr_internal_readerattr_internal_writerattr_internal_accessor。它們的行為與 Ruby 內建的 attr_* 對應項類似,不同之處在於它們會以降低衝突可能性方式命名底層的實例變數。

巨集 attr_internalattr_internal_accessor 的同義詞。

# library
class ThirdPartyLibrary::Crawler
  attr_internal :log_level
end

# client code
class MyCrawler < ThirdPartyLibrary::Crawler
  attr_accessor :log_level
end

在先前的範例中,可能的情況是 :log_level 不屬於函式庫的公開介面,它僅用於開發。用戶端程式碼在不知情潛在衝突的情況下,對其進行子類化並定義自己的 :log_level。幸好有 attr_internal,不會發生衝突。

預設情況下,內部實例變數會使用底線開頭命名,以上述範例為例,為 @_log_level。雖然可以透過 Module.attr_internal_naming_format 設定,但您可以傳遞任何帶有開頭 @ 和某處的 %s 的類 sprintf 格式字串,其中將放置名稱。預設值為 "@_%s"

Rails 在一些地方使用內部屬性,例如在檢視中

module ActionView
  class Base
    attr_internal :captures
    attr_internal :request, :layout
    attr_internal :controller, :template
  end
end

3.1.3 模組屬性

巨集 mattr_readermattr_writermattr_accessor 與為類別定義的 cattr_* 巨集相同。實際上,cattr_* 巨集只是 mattr_* 巨集的別名。請查看類別屬性

例如,Active Storage 的 logger API 是使用 mattr_accessor 產生的

module ActiveStorage
  mattr_accessor :logger
end

3.2 父層 (Parents)

3.2.1 module_parent

巢狀命名模組上的 module_parent 方法會回傳包含其對應常數的模組。

module X
  module Y
    module Z
    end
  end
end
M = X::Y::Z

X::Y::Z.module_parent # => X::Y
M.module_parent       # => X::Y

如果模組是匿名的或屬於最上層,module_parent 會回傳 Object

請注意,在這種情況下,module_parent_name 會回傳 nil

3.2.2 module_parent_name

巢狀命名模組上的 module_parent_name 方法會回傳包含其對應常數的模組的完整名稱。

module X
  module Y
    module Z
    end
  end
end
M = X::Y::Z

X::Y::Z.module_parent_name # => "X::Y"
M.module_parent_name       # => "X::Y"

對於最上層或匿名模組,module_parent_name 會回傳 nil

請注意,在這種情況下,module_parent 會回傳 Object

3.2.3 module_parents

module_parents 方法會在接收者上呼叫 module_parent,並向上追溯到 Object。該鏈會以陣列形式回傳,從最底層到最上層。

module X
  module Y
    module Z
    end
  end
end
M = X::Y::Z

X::Y::Z.module_parents # => [X::Y, X, Object]
M.module_parents       # => [X::Y, X, Object]

3.3 匿名 (Anonymous)

一個模組可能有名稱,也可能沒有名稱。

module M
end
M.name # => "M"

N = Module.new
N.name # => "N"

Module.new.name # => nil

您可以使用 anonymous? 述詞來檢查模組是否有名稱。

module M
end
M.anonymous? # => false

Module.new.anonymous? # => true

請注意,無法存取並不表示是匿名的。

module M
end

m = Object.send(:remove_const, :M)

m.anonymous? # => false

儘管匿名模組在定義上是無法存取的。

3.4 方法委派 (Method Delegation)

3.4.1 delegate

巨集 delegate 提供了一個簡單的方法來轉發方法。

假設在某些應用程式中,使用者在 User 模型中有登入資訊,但在另一個 Profile 模型中有名稱和其他資料。

class User < ApplicationRecord
  has_one :profile
end

透過該配置,您可以透過使用者的個人資料 user.profile.name 來取得使用者的名稱,但如果可以直接存取此屬性會很方便。

class User < ApplicationRecord
  has_one :profile

  def name
    profile.name
  end
end

這就是 delegate 為您做的事。

class User < ApplicationRecord
  has_one :profile

  delegate :name, to: :profile
end

它更簡潔,並且意圖更明顯。

該方法在目標中必須是公開的。

delegate 巨集接受多個方法。

delegate :name, :age, :address, :twitter, to: :profile

當插入字串時,:to 選項應該變成一個運算式,該運算式會評估為方法委派到的物件。通常是字串或符號。此運算式會在接收者的上下文中評估。

# delegates to the Rails constant
delegate :logger, to: :Rails

# delegates to the receiver's class
delegate :table_name, to: :class

如果 :prefix 選項是 true,則此選項的通用性較低,請參閱下文。

預設情況下,如果委派引發 NoMethodError 且目標為 nil,則例外狀況會被傳播。您可以使用 :allow_nil 選項要求回傳 nil 來取代。

delegate :name, to: :profile, allow_nil: true

使用 :allow_nil 時,如果使用者沒有個人資料,則呼叫 user.name 會回傳 nil

:prefix 選項會為產生的方法名稱新增前綴。例如,這對於取得更好的名稱可能很方便。

delegate :street, to: :address, prefix: true

先前的範例會產生 address_street 而不是 street

由於在這種情況下,產生的方法名稱是由目標物件和目標方法名稱組成,因此 :to 選項必須是方法名稱。

也可以配置自訂前綴。

delegate :size, to: :attachment, prefix: :avatar

在先前的範例中,巨集會產生 avatar_size 而不是 size

:private 選項會變更方法範圍。

delegate :date_of_birth, to: :profile, private: true

委派的方法預設為公開。傳遞 private: true 來變更它。

3.4.2 delegate_missing_to

假設您想要將 User 物件中遺失的所有內容委派給 Profile 物件。巨集 delegate_missing_to 可讓您輕鬆實現此目的。

class User < ApplicationRecord
  has_one :profile

  delegate_missing_to :profile
end

目標可以是物件內可呼叫的任何內容,例如實例變數、方法、常數等。只有目標的公開方法才會被委派。

3.5 重新定義方法 (Redefining Methods)

在某些情況下,您需要使用 define_method 定義方法,但不知道是否已存在具有該名稱的方法。如果存在,則在啟用時會發出警告。這不是什麼大問題,但也並不乾淨。

方法 redefine_method 可防止這種潛在的警告,如果需要,會先移除現有的方法。

如果您需要自行定義替換方法(例如,因為您正在使用 delegate),您也可以使用 silence_redefinition_of_method

4 Class 的擴充 (Extensions to Class)

4.1 類別屬性 (Class Attributes)

4.1.1 class_attribute

方法 class_attribute 宣告一個或多個可繼承的類別屬性,這些屬性可以在階層結構中的任何層級覆寫。

class A
  class_attribute :x
end

class B < A; end

class C < B; end

A.x = :a
B.x # => :a
C.x # => :a

B.x = :b
A.x # => :a
C.x # => :b

C.x = :c
A.x # => :a
B.x # => :b

例如,ActionMailer::Base 定義了

class_attribute :default_params
self.default_params = {
  mime_version: "1.0",
  charset: "UTF-8",
  content_type: "text/plain",
  parts_order: [ "text/plain", "text/enriched", "text/html" ]
}.freeze

它們也可以在實例層級存取和覆寫。

A.x = 1

a1 = A.new
a2 = A.new
a2.x = 2

a1.x # => 1, comes from A
a2.x # => 2, overridden in a2

可以透過將選項 :instance_writer 設定為 false 來防止產生寫入器實例方法。

module ActiveRecord
  class Base
    class_attribute :table_name_prefix, instance_writer: false, default: "my"
  end
end

模型可能會發現該選項很有用,可用來防止大量指派設定屬性。

可以透過將選項 :instance_reader 設定為 false 來防止產生讀取器實例方法。

class A
  class_attribute :x, instance_reader: false
end

A.new.x = 1
A.new.x # NoMethodError

為了方便起見,class_attribute 還定義了一個實例述詞,它是實例讀取器回傳值的雙重否定。在上面的範例中,它會被稱為 x?

:instance_readerfalse 時,實例述詞會像讀取器方法一樣回傳 NoMethodError

如果您不想要實例述詞,請傳遞 instance_predicate: false,它就不會被定義。

4.1.2 cattr_readercattr_writercattr_accessor

巨集 cattr_readercattr_writercattr_accessor 與其 attr_* 對應物類似,但適用於類別。它們會將類別變數初始化為 nil,除非它已存在,並產生相應的類別方法來存取它。

class MysqlAdapter < AbstractAdapter
  # Generates class methods to access @@emulate_booleans.
  cattr_accessor :emulate_booleans
end

此外,您可以將區塊傳遞給 cattr_* 以使用預設值設定屬性。

class MysqlAdapter < AbstractAdapter
  # Generates class methods to access @@emulate_booleans with default value of true.
  cattr_accessor :emulate_booleans, default: true
end

為了方便起見,也會建立實例方法,它們只是類別屬性的代理。因此,實例可以變更類別屬性,但無法像 class_attribute 那樣覆寫它(請參閱上文)。例如,給定

module ActionView
  class Base
    cattr_accessor :field_error_proc, default: Proc.new {
      # ...
    }
  end
end

我們可以在視圖中存取 field_error_proc

可以透過將 :instance_reader 設定為 false 來防止產生讀取器實例方法,並且可以透過將 :instance_writer 設定為 false 來防止產生寫入器實例方法。可以透過將 :instance_accessor 設定為 false 來防止產生這兩種方法。在所有情況下,該值都必須正好是 false,而不是任何 false 值。

module A
  class B
    # No first_name instance reader is generated.
    cattr_accessor :first_name, instance_reader: false
    # No last_name= instance writer is generated.
    cattr_accessor :last_name, instance_writer: false
    # No surname instance reader or surname= writer is generated.
    cattr_accessor :surname, instance_accessor: false
  end
end

模型可能會發現將 :instance_accessor 設定為 false 很有用,可用來防止大量指派設定屬性。

4.2 子類別和後代 (Subclasses and Descendants)

4.2.1 subclasses

方法 subclasses 會回傳接收者的子類別。

class C; end
C.subclasses # => []

class B < C; end
C.subclasses # => [B]

class A < B; end
C.subclasses # => [B]

class D < C; end
C.subclasses # => [B, D]

這些類別回傳的順序未指定。

4.2.2 descendants

方法 descendants 會回傳所有 < 其接收者的類別。

class C; end
C.descendants # => []

class B < C; end
C.descendants # => [B]

class A < B; end
C.descendants # => [B, A]

class D < C; end
C.descendants # => [B, A, D]

這些類別回傳的順序未指定。

5 String 的擴充 (Extensions to String)

5.1 輸出安全性 (Output Safety)

5.1.1 動機 (Motivation)

將資料插入 HTML 範本需要格外小心。例如,您不能只是逐字將 @review.title 插入 HTML 頁面。首先,如果評論標題是「Flanagan & Matz rules!」,則輸出將不會格式正確,因為 & 符號必須逸出為 "&amp;"。此外,根據應用程式的不同,這可能是一個很大的安全漏洞,因為使用者可以透過設定精心製作的評論標題來注入惡意 HTML。有關風險的更多資訊,請參閱安全性指南中關於跨網站指令碼的部分。

5.1.2 安全字串 (Safe Strings)

Active Support 具有(html) 安全字串的概念。安全字串是標記為可按原樣插入 HTML 中的字串。它是受信任的,無論它是否已逸出。

字串預設被視為不安全

"".html_safe? # => false

您可以使用 html_safe 方法從給定的字串取得安全字串。

s = "".html_safe
s.html_safe? # => true

重要的是要理解 html_safe 不會執行任何逸出,它只是一個斷言。

s = "<script>...</script>".html_safe
s.html_safe? # => true
s            # => "<script>...</script>"

您有責任確保在特定字串上呼叫 html_safe 是沒有問題的。

如果您附加到安全字串,無論是使用 concat/<< 就地附加,還是使用 + 附加,結果都會是安全字串。不安全的引數會逸出。

"".html_safe + "<" # => "&lt;"

安全引數會直接附加。

"".html_safe + "<".html_safe # => "<"

這些方法不應在普通的視圖中使用。不安全的值會自動逸出。

<%= @review.title %> <%# fine, escaped if needed %>

若要按原樣插入某些內容,請使用 raw 輔助方法,而不是呼叫 html_safe

<%= raw @cms.current_template %> <%# inserts @cms.current_template as is %>

或者,等效地,使用 <%==

<%== @cms.current_template %> <%# inserts @cms.current_template as is %>

raw 輔助方法會為您呼叫 html_safe

def raw(stringish)
  stringish.to_s.html_safe
end

5.1.3 轉換 (Transformation)

一般來說,除了上面說明的串連之外,任何可能會變更字串的方法都會給您一個不安全的字串。這些是 downcasegsubstripchompunderscore 等。

在像 gsub! 這樣就地轉換的情況下,接收者本身會變得不安全。

無論轉換是否實際變更了任何內容,安全性位元都會永遠遺失。

5.1.4 轉換和強制型別轉換 (Conversion and Coercion)

在安全字串上呼叫 to_s 會回傳安全字串,但使用 to_str 強制型別轉換會回傳不安全的字串。

5.1.5 複製 (Copying)

在安全字串上呼叫 dupclone 會產生安全字串。

5.2 remove

方法 remove 會移除所有出現的模式。

"Hello World".remove(/Hello /) # => "World"

還有破壞性的版本 String#remove!

5.3 squish

方法 squish 會移除前導和尾隨空白,並將連續的空白替換為單一空格。

" \n  foo\n\r \t bar \n".squish # => "foo bar"

還有破壞性的版本 String#squish!

請注意,它可以處理 ASCII 和 Unicode 空白。

5.4 truncate

方法 truncate 會回傳接收者的複本,在給定的 length 後截斷。

"Oh dear! Oh dear! I shall be late!".truncate(20)
# => "Oh dear! Oh dear!..."

省略符號可以使用 :omission 選項進行自訂。

"Oh dear! Oh dear! I shall be late!".truncate(20, omission: "&hellip;")
# => "Oh dear! Oh &hellip;"

請特別注意,截斷會將省略字串的長度納入考量。

傳遞一個 :separator 選項,可以在自然斷點處截斷字串。

"Oh dear! Oh dear! I shall be late!".truncate(18)
# => "Oh dear! Oh dea..."
"Oh dear! Oh dear! I shall be late!".truncate(18, separator: " ")
# => "Oh dear! Oh..."

:separator 選項可以是一個正規表示式。

"Oh dear! Oh dear! I shall be late!".truncate(18, separator: /\s/)
# => "Oh dear! Oh..."

在上面的範例中,「dear」會先被截斷,但接著 :separator 選項阻止了這件事。

5.5 truncate_bytes

方法 truncate_bytes 會返回接收者的副本,該副本被截斷為最多 bytesize 個位元組。

"👍👍👍👍".truncate_bytes(15)
# => "👍👍👍…"

省略符號可以使用 :omission 選項進行自訂。

"👍👍👍👍".truncate_bytes(15, omission: "🖖")
# => "👍👍🖖"

5.6 truncate_words

方法 truncate_words 會返回接收者的副本,該副本在給定單字數後被截斷。

"Oh dear! Oh dear! I shall be late!".truncate_words(4)
# => "Oh dear! Oh dear!..."

省略符號可以使用 :omission 選項進行自訂。

"Oh dear! Oh dear! I shall be late!".truncate_words(4, omission: "&hellip;")
# => "Oh dear! Oh dear!&hellip;"

傳遞一個 :separator 選項,可以在自然斷點處截斷字串。

"Oh dear! Oh dear! I shall be late!".truncate_words(3, separator: "!")
# => "Oh dear! Oh dear! I shall be late..."

:separator 選項可以是一個正規表示式。

"Oh dear! Oh dear! I shall be late!".truncate_words(4, separator: /\s/)
# => "Oh dear! Oh dear!..."

5.7 inquiry

方法 inquiry 將字串轉換為 StringInquirer 物件,使等式檢查更加簡潔。

"production".inquiry.production? # => true
"active".inquiry.inactive?       # => false

5.8 starts_with?ends_with?

Active Support 定義了 String#start_with?String#end_with? 的第三人稱別名。

"foo".starts_with?("f") # => true
"foo".ends_with?("o")   # => true

5.9 strip_heredoc

方法 strip_heredoc 會移除 heredoc 中的縮排。

例如在

if options[:usage]
  puts <<-USAGE.strip_heredoc
    This command does such and such.

    Supported options are:
      -h         This message
      ...
  USAGE
end

使用者將會看到靠左對齊的使用訊息。

技術上來說,它會尋找整個字串中縮排最少的行,並移除該數量的開頭空白。

5.10 indent

方法 indent 會縮排接收者中的行。

<<EOS.indent(2)
def some_method
  some_code
end
EOS
# =>
  def some_method
    some_code
  end

第二個參數 indent_string 指定要使用的縮排字串。預設值為 nil,這會告訴方法去觀察第一個縮排的行來進行判斷,如果沒有則回退到空格。

"  foo".indent(2)        # => "    foo"
"foo\n\t\tbar".indent(2) # => "\t\tfoo\n\t\t\t\tbar"
"foo".indent(2, "\t")    # => "\t\tfoo"

雖然 indent_string 通常是一個空格或 tab,但它可以是任何字串。

第三個參數 indent_empty_lines 是一個旗標,表示是否應該縮排空白行。預設值為 false。

"foo\n\nbar".indent(2)            # => "  foo\n\n  bar"
"foo\n\nbar".indent(2, nil, true) # => "  foo\n  \n  bar"

方法 indent! 會就地執行縮排。

5.11 存取

5.11.1 at(position)

方法 at 會返回字串中位置 position 的字元。

"hello".at(0)  # => "h"
"hello".at(4)  # => "o"
"hello".at(-1) # => "o"
"hello".at(10) # => nil

5.11.2 from(position)

方法 from 會返回字串中從位置 position 開始的子字串。

"hello".from(0)  # => "hello"
"hello".from(2)  # => "llo"
"hello".from(-2) # => "lo"
"hello".from(10) # => nil

5.11.3 to(position)

方法 to 會返回字串中到位置 position 的子字串。

"hello".to(0)  # => "h"
"hello".to(2)  # => "hel"
"hello".to(-2) # => "hell"
"hello".to(10) # => "hello"

5.11.4 first(limit = 1)

方法 first 會返回包含字串前 limit 個字元的子字串。

如果 n > 0,則呼叫 str.first(n) 等同於 str.to(n-1),且當 n == 0 時返回空字串。

5.11.5 last(limit = 1)

方法 last 會返回包含字串最後 limit 個字元的子字串。

如果 n > 0,則呼叫 str.last(n) 等同於 str.from(-n),且當 n == 0 時返回空字串。

5.12 詞形變化

5.12.1 pluralize

方法 pluralize 會返回接收者的複數形式。

"table".pluralize     # => "tables"
"ruby".pluralize      # => "rubies"
"equipment".pluralize # => "equipment"

如先前的範例所示,Active Support 知道一些不規則的複數和不可數名詞。內建規則可以在 config/initializers/inflections.rb 中擴展。此檔案預設由 rails new 命令產生,並且在註解中包含說明。

pluralize 也可以接受一個可選的 count 參數。如果 count == 1,則會返回單數形式。對於 count 的任何其他值,則會返回複數形式。

"dude".pluralize(0) # => "dudes"
"dude".pluralize(1) # => "dude"
"dude".pluralize(2) # => "dudes"

Active Record 使用此方法來計算對應於模型的預設資料表名稱。

# active_record/model_schema.rb
def undecorated_table_name(model_name)
  table_name = model_name.to_s.demodulize.underscore
  pluralize_table_names ? table_name.pluralize : table_name
end

5.12.2 singularize

方法 singularizepluralize 的反向操作。

"tables".singularize    # => "table"
"rubies".singularize    # => "ruby"
"equipment".singularize # => "equipment"

關聯會使用此方法計算對應的預設關聯類別的名稱。

# active_record/reflection.rb
def derive_class_name
  class_name = name.to_s.camelize
  class_name = class_name.singularize if collection?
  class_name
end

5.12.3 camelize

方法 camelize 會以駝峰式大小寫返回接收者。

"product".camelize    # => "Product"
"admin_user".camelize # => "AdminUser"

作為經驗法則,您可以將此方法視為將路徑轉換為 Ruby 類別或模組名稱的方法,其中斜線分隔命名空間。

"backoffice/session".camelize # => "Backoffice::Session"

例如,Action Pack 使用此方法來載入提供特定會話儲存的類別。

# action_controller/metal/session_management.rb
def session_store=(store)
  @@session_store = store.is_a?(Symbol) ?
    ActionDispatch::Session.const_get(store.to_s.camelize) :
    store
end

camelize 接受一個可選參數,它可以是 :upper(預設值)或 :lower。使用後者時,第一個字母會變成小寫。

"visual_effect".camelize(:lower) # => "visualEffect"

這可能方便在遵循該慣例的語言(例如 JavaScript)中計算方法名稱。

作為經驗法則,您可以將 camelize 視為 underscore 的反向操作,儘管在某些情況下並非如此:"SSLError".underscore.camelize 會返回 "SslError"。為了支援這種情況,Active Support 允許您在 config/initializers/inflections.rb 中指定縮寫。

ActiveSupport::Inflector.inflections do |inflect|
  inflect.acronym "SSL"
end

"SSLError".underscore.camelize # => "SSLError"

camelize 的別名為 camelcase

5.12.4 underscore

方法 underscore 會反向操作,從駝峰式大小寫轉換為路徑。

"Product".underscore   # => "product"
"AdminUser".underscore # => "admin_user"

也會將 "::" 轉換回 "/"。

"Backoffice::Session".underscore # => "backoffice/session"

並且理解以小寫字母開頭的字串。

"visualEffect".underscore # => "visual_effect"

underscore 不接受任何參數。

Rails 使用 underscore 來取得控制器類別的小寫名稱。

# actionpack/lib/abstract_controller/base.rb
def controller_path
  @controller_path ||= name.delete_suffix("Controller").underscore
end

例如,您在 params[:controller] 中獲得的值就是該值。

作為經驗法則,您可以將 underscore 視為 camelize 的反向操作,儘管在某些情況下並非如此。例如,"SSLError".underscore.camelize 會返回 "SslError"

5.12.5 titleize

方法 titleize 會將接收者中的單字大寫。

"alice in wonderland".titleize # => "Alice In Wonderland"
"fermat's enigma".titleize     # => "Fermat's Enigma"

titleize 的別名為 titlecase

5.12.6 dasherize

方法 dasherize 會將接收者中的底線替換為破折號。

"name".dasherize         # => "name"
"contact_data".dasherize # => "contact-data"

模型的 XML 序列化器使用此方法來將節點名稱加上破折號。

# active_model/serializers/xml.rb
def reformat_name(name)
  name = name.camelize if camelize?
  dasherize? ? name.dasherize : name
end

5.12.7 demodulize

給定一個帶有完整常數名稱的字串,demodulize 會返回實際常數名稱,即最右邊的部分。

"Product".demodulize                        # => "Product"
"Backoffice::UsersController".demodulize    # => "UsersController"
"Admin::Hotel::ReservationUtils".demodulize # => "ReservationUtils"
"::Inflections".demodulize                  # => "Inflections"
"".demodulize                               # => ""

例如,Active Record 使用此方法來計算計數器快取欄位的名稱。

# active_record/reflection.rb
def counter_cache_column
  if options[:counter_cache] == true
    "#{active_record.name.demodulize.underscore.pluralize}_count"
  elsif options[:counter_cache]
    options[:counter_cache]
  end
end

5.12.8 deconstantize

給定一個帶有完整常數參考運算式的字串,deconstantize 會移除最右邊的部分,通常會留下常數容器的名稱。

"Product".deconstantize                        # => ""
"Backoffice::UsersController".deconstantize    # => "Backoffice"
"Admin::Hotel::ReservationUtils".deconstantize # => "Admin::Hotel"

5.12.9 parameterize

方法 parameterize 會以可用於美觀 URL 的方式正規化接收者。

"John Smith".parameterize # => "john-smith"
"Kurt Gödel".parameterize # => "kurt-godel"

要保留字串的大小寫,請將 preserve_case 參數設定為 true。預設情況下,preserve_case 設定為 false。

"John Smith".parameterize(preserve_case: true) # => "John-Smith"
"Kurt Gödel".parameterize(preserve_case: true) # => "Kurt-Godel"

要使用自訂分隔符號,請覆寫 separator 參數。

"John Smith".parameterize(separator: "_") # => "john_smith"
"Kurt Gödel".parameterize(separator: "_") # => "kurt_godel"

5.12.10 tableize

方法 tableizeunderscore 後面接著 pluralize

"Person".tableize      # => "people"
"Invoice".tableize     # => "invoices"
"InvoiceLine".tableize # => "invoice_lines"

作為經驗法則,在簡單的情況下,tableize 會返回對應於給定模型的資料表名稱。實際上 Active Record 中的實作並非直接使用 tableize,因為它還會將類別名稱 demodulize,並檢查一些可能會影響返回字串的選項。

5.12.11 classify

方法 classifytableize 的反向操作。它會為您提供對應於資料表名稱的類別名稱。

"people".classify        # => "Person"
"invoices".classify      # => "Invoice"
"invoice_lines".classify # => "InvoiceLine"

該方法會理解完整資料表名稱。

"highrise_production.companies".classify # => "Company"

請注意,classify 會以字串形式返回類別名稱。您可以透過對其呼叫 constantize 來取得實際的類別物件,這會在接下來說明。

5.12.12 constantize

方法 constantize 會解析接收者中的常數參考運算式。

"Integer".constantize # => Integer

module M
  X = 1
end
"M::X".constantize # => 1

如果字串評估為未知的常數,或其內容甚至不是有效的常數名稱,則 constantize 會引發 NameError

constantize 的常數名稱解析始終從最上層的 Object 開始,即使沒有前導的 "::"。

X = :in_Object
module M
  X = :in_M

  X                 # => :in_M
  "::X".constantize # => :in_Object
  "X".constantize   # => :in_Object (!)
end

因此,它通常不等同於 Ruby 在相同位置評估實際常數時所做的操作。

Mailer 測試案例使用 constantize 從測試類別的名稱中取得正在測試的郵件程式。

# action_mailer/test_case.rb
def determine_default_mailer(name)
  name.delete_suffix("Test").constantize
rescue NameError => e
  raise NonInferrableMailerError.new(name)
end

5.12.13 humanize

方法 humanize 會調整屬性名稱以顯示給最終使用者。

具體來說,它會執行以下轉換:

  • 將人類詞形變化規則應用於參數。
  • 刪除開頭的底線(如果有的話)。
  • 移除 "_id" 後綴(如果有的話)。
  • 將底線替換為空格(如果有的話)。
  • 將所有單字轉換為小寫,除了縮寫。
  • 將第一個單字的首字母大寫。

可以透過將 :capitalize 選項設定為 false 來關閉第一個單字的首字母大寫功能(預設為 true)。

"name".humanize                         # => "Name"
"author_id".humanize                    # => "Author"
"author_id".humanize(capitalize: false) # => "author"
"comments_count".humanize               # => "Comments count"
"_id".humanize                          # => "Id"

如果將 "SSL" 定義為縮寫

"ssl_error".humanize # => "SSL error"

輔助方法 full_messages 使用 humanize 作為後備方案,以包含屬性名稱

def full_messages
  map { |attribute, message| full_message(attribute, message) }
end

def full_message
  # ...
  attr_name = attribute.to_s.tr(".", "_").humanize
  attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
  # ...
end

5.12.14 foreign_key

方法 foreign_key 從類別名稱給出一個外鍵欄位名稱。為此,它會進行 demudulize、使用底線,並加上 "_id"

"User".foreign_key           # => "user_id"
"InvoiceLine".foreign_key    # => "invoice_line_id"
"Admin::Session".foreign_key # => "session_id"

如果您不想要 "_id" 中的底線,請傳遞 false 參數

"User".foreign_key(false) # => "userid"

關聯會使用此方法來推斷外鍵,例如 has_onehas_many 會這樣做

# active_record/associations.rb
foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key

5.12.15 upcase_first

方法 upcase_first 將接收者的第一個字母轉換為大寫

"employee salary".upcase_first # => "Employee salary"
"".upcase_first                # => ""

5.12.16 downcase_first

方法 downcase_first 將接收者的第一個字母轉換為小寫

"If I had read Alice in Wonderland".downcase_first # => "if I had read Alice in Wonderland"
"".downcase_first                                  # => ""

5.13 轉換

5.13.1 to_dateto_timeto_datetime

方法 to_dateto_timeto_datetime 基本上是 Date._parse 的便利包裝器

"2010-07-27".to_date              # => Tue, 27 Jul 2010
"2010-07-27 23:37:00".to_time     # => 2010-07-27 23:37:00 +0200
"2010-07-27 23:37:00".to_datetime # => Tue, 27 Jul 2010 23:37:00 +0000

to_time 接收一個可選參數 :utc:local,以指示您想要的時間時區

"2010-07-27 23:42:00".to_time(:utc)   # => 2010-07-27 23:42:00 UTC
"2010-07-27 23:42:00".to_time(:local) # => 2010-07-27 23:42:00 +0200

預設值為 :local

請參閱 Date._parse 的文件以取得更多詳細資訊。

它們三個都會為空白的接收者返回 nil

6 Symbol 的擴充

6.1 starts_with?ends_with?

Active Support 定義了 Symbol#start_with?Symbol#end_with? 的第三人稱別名

:foo.starts_with?("f") # => true
:foo.ends_with?("o")   # => true

7 Numeric 的擴充

7.1 位元組

所有數字都會回應這些方法

它們使用 1024 的轉換因子返回對應的位元組數

2.kilobytes   # => 2048
3.megabytes   # => 3145728
3.5.gigabytes # => 3758096384.0
-4.exabytes   # => -4611686018427387904

單數形式有別名,因此您可以說

1.megabyte # => 1048576

7.2 時間

以下方法

啟用時間宣告和計算,例如 45.minutes + 2.hours + 4.weeks。它們的返回值也可以加到或減去 Time 物件。

這些方法可以與 from_nowago 等組合使用,以進行精確的日期計算。例如

# equivalent to Time.current.advance(days: 1)
1.day.from_now

# equivalent to Time.current.advance(weeks: 2)
2.weeks.from_now

# equivalent to Time.current.advance(days: 4, weeks: 5)
(4.days + 5.weeks).from_now

如需其他持續時間,請參閱 Integer 的時間擴充。

7.3 格式化

啟用以各種方式格式化數字。

產生一個數字作為電話號碼的字串表示形式

5551234.to_fs(:phone)
# => 555-1234
1235551234.to_fs(:phone)
# => 123-555-1234
1235551234.to_fs(:phone, area_code: true)
# => (123) 555-1234
1235551234.to_fs(:phone, delimiter: " ")
# => 123 555 1234
1235551234.to_fs(:phone, area_code: true, extension: 555)
# => (123) 555-1234 x 555
1235551234.to_fs(:phone, country_code: 1)
# => +1-123-555-1234

產生一個數字作為貨幣的字串表示形式

1234567890.50.to_fs(:currency)                 # => $1,234,567,890.50
1234567890.506.to_fs(:currency)                # => $1,234,567,890.51
1234567890.506.to_fs(:currency, precision: 3)  # => $1,234,567,890.506

產生一個數字作為百分比的字串表示形式

100.to_fs(:percentage)
# => 100.000%
100.to_fs(:percentage, precision: 0)
# => 100%
1000.to_fs(:percentage, delimiter: ".", separator: ",")
# => 1.000,000%
302.24398923423.to_fs(:percentage, precision: 5)
# => 302.24399%

產生一個數字作為分隔形式的字串表示形式

12345678.to_fs(:delimited)                     # => 12,345,678
12345678.05.to_fs(:delimited)                  # => 12,345,678.05
12345678.to_fs(:delimited, delimiter: ".")     # => 12.345.678
12345678.to_fs(:delimited, delimiter: ",")     # => 12,345,678
12345678.05.to_fs(:delimited, separator: " ")  # => 12,345,678 05

產生一個數字四捨五入到指定精度的字串表示形式

111.2345.to_fs(:rounded)                     # => 111.235
111.2345.to_fs(:rounded, precision: 2)       # => 111.23
13.to_fs(:rounded, precision: 5)             # => 13.00000
389.32314.to_fs(:rounded, precision: 0)      # => 389
111.2345.to_fs(:rounded, significant: true)  # => 111

產生一個數字作為人類可讀位元組數的字串表示形式

123.to_fs(:human_size)                  # => 123 Bytes
1234.to_fs(:human_size)                 # => 1.21 KB
12345.to_fs(:human_size)                # => 12.1 KB
1234567.to_fs(:human_size)              # => 1.18 MB
1234567890.to_fs(:human_size)           # => 1.15 GB
1234567890123.to_fs(:human_size)        # => 1.12 TB
1234567890123456.to_fs(:human_size)     # => 1.1 PB
1234567890123456789.to_fs(:human_size)  # => 1.07 EB

產生一個數字作為人類可讀單字的字串表示形式

123.to_fs(:human)               # => "123"
1234.to_fs(:human)              # => "1.23 Thousand"
12345.to_fs(:human)             # => "12.3 Thousand"
1234567.to_fs(:human)           # => "1.23 Million"
1234567890.to_fs(:human)        # => "1.23 Billion"
1234567890123.to_fs(:human)     # => "1.23 Trillion"
1234567890123456.to_fs(:human)  # => "1.23 Quadrillion"

8 Integer 的擴充

8.1 multiple_of?

方法 multiple_of? 測試整數是否為參數的倍數

2.multiple_of?(1) # => true
1.multiple_of?(2) # => false

8.2 ordinal

方法 ordinal 返回對應於接收整數的序數後綴字串

1.ordinal    # => "st"
2.ordinal    # => "nd"
53.ordinal   # => "rd"
2009.ordinal # => "th"
-21.ordinal  # => "st"
-134.ordinal # => "th"

8.3 ordinalize

方法 ordinalize 返回對應於接收整數的序數字串。相比之下,請注意 ordinal 方法**僅**返回後綴字串。

1.ordinalize    # => "1st"
2.ordinalize    # => "2nd"
53.ordinalize   # => "53rd"
2009.ordinalize # => "2009th"
-21.ordinalize  # => "-21st"
-134.ordinalize # => "-134th"

8.4 時間

以下方法

啟用時間宣告和計算,例如 4.months + 5.years。它們的返回值也可以加到或減去 Time 物件。

這些方法可以與 from_nowago 等組合使用,以進行精確的日期計算。例如

# equivalent to Time.current.advance(months: 1)
1.month.from_now

# equivalent to Time.current.advance(years: 2)
2.years.from_now

# equivalent to Time.current.advance(months: 4, years: 5)
(4.months + 5.years).from_now

如需其他持續時間,請參閱 Numeric 的時間擴充。

9 BigDecimal 的擴充

9.1 to_s

方法 to_s 提供 "F" 的預設指定符。這表示簡單調用 to_s 將產生浮點數表示法,而不是工程符號

BigDecimal(5.00, 6).to_s       # => "5.0"

仍然支援工程符號

BigDecimal(5.00, 6).to_s("e")  # => "0.5E1"

10 Enumerable 的擴充

10.1 index_by

方法 index_by 產生一個雜湊,其中可列舉的元素由某個鍵索引。

它會遍歷集合並將每個元素傳遞給區塊。該元素將由區塊返回的值鍵控

invoices.index_by(&:number)
# => {"2009-032" => <Invoice ...>, "2009-008" => <Invoice ...>, ...}

鍵通常應該是唯一的。如果區塊為不同的元素返回相同的值,則不會為該鍵建立集合。最後一個項目將獲勝。

10.2 index_with

方法 index_with 產生一個雜湊,其中可列舉的元素作為鍵。值是傳遞的預設值或在區塊中返回的值。

post = Post.new(title: "hey there", body: "what's up?")

%i( title body ).index_with { |attr_name| post.public_send(attr_name) }
# => { title: "hey there", body: "what's up?" }

WEEKDAYS.index_with(Interval.all_day)
# => { monday: [ 0, 1440 ], … }

10.3 many?

方法 many?collection.size > 1 的簡寫

<% if pages.many? %>
  <%= pagination_links %>
<% end %>

如果給定一個可選區塊,many? 只會考慮返回 true 的那些元素

@see_more = videos.many? { |video| video.category == params[:category] }

10.4 exclude?

謂詞 exclude? 測試給定的物件是否**不**屬於集合。它是內建的 include? 的否定

to_visit << node if visited.exclude?(node)

10.5 including

方法 including 返回一個新的可列舉物件,其中包含傳遞的元素

[ 1, 2, 3 ].including(4, 5)                    # => [ 1, 2, 3, 4, 5 ]
["David", "Rafael"].including %w[ Aaron Todd ] # => ["David", "Rafael", "Aaron", "Todd"]

10.6 excluding

方法 excluding 返回可列舉物件的副本,並移除指定的元素

["David", "Rafael", "Aaron", "Todd"].excluding("Aaron", "Todd") # => ["David", "Rafael"]

excludingwithout 的別名。

10.7 pluck

方法 pluck 從每個元素中提取給定的鍵

[{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pluck(:name) # => ["David", "Rafael", "Aaron"]
[{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pluck(:id, :name) # => [[1, "David"], [2, "Rafael"]]

10.8 pick

方法 pick 從第一個元素中提取給定的鍵

[{ name: "David" }, { name: "Rafael" }, { name: "Aaron" }].pick(:name) # => "David"
[{ id: 1, name: "David" }, { id: 2, name: "Rafael" }].pick(:id, :name) # => [1, "David"]

11 Array 的擴充

11.1 存取

Active Support 增強了陣列的 API,以簡化某些存取方式。例如,to 返回直到傳遞索引處的元素的子陣列

%w(a b c d).to(2) # => ["a", "b", "c"]
[].to(7)          # => []

同樣,from 返回從傳遞索引處的元素到結尾的尾部。如果索引大於陣列的長度,則返回空陣列。

%w(a b c d).from(2)  # => ["c", "d"]
%w(a b c d).from(10) # => []
[].from(0)           # => []

方法 including 返回一個包含傳遞元素的新陣列

[ 1, 2, 3 ].including(4, 5)          # => [ 1, 2, 3, 4, 5 ]
[ [ 0, 1 ] ].including([ [ 1, 0 ] ]) # => [ [ 0, 1 ], [ 1, 0 ] ]

方法 excluding 返回一個陣列的副本,其中排除指定的元素。這是 Enumerable#excluding 的最佳化,它使用 Array#- 而不是 Array#reject,以提高效能。

["David", "Rafael", "Aaron", "Todd"].excluding("Aaron", "Todd") # => ["David", "Rafael"]
[ [ 0, 1 ], [ 1, 0 ] ].excluding([ [ 1, 0 ] ])                  # => [ [ 0, 1 ] ]

方法 secondthirdfourthfifth 返回對應的元素,second_to_lastthird_to_last 也是如此(firstlast 是內建的)。感謝社會智慧和積極的建設性,forty_two 也可用。

%w(a b c d).third # => "c"
%w(a b c d).fifth # => nil

11.2 提取

方法 extract! 移除並返回區塊返回 true 值的元素。如果沒有給定區塊,則會返回 Enumerator。

numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
odd_numbers = numbers.extract! { |number| number.odd? } # => [1, 3, 5, 7, 9]
numbers # => [0, 2, 4, 6, 8]

11.3 選項提取

當方法呼叫中的最後一個引數是雜湊時,也許除了一個 &block 引數外,Ruby 允許您省略括號

User.exists?(email: params[:email])

在 Rails 中大量使用這種語法糖,以避免位置引數過多的情況,而是提供模擬命名參數的介面。特別是,使用尾隨雜湊作為選項是非常慣用的。

但是,如果方法期望可變數量的引數並在其宣告中使用 *,則此類選項雜湊最終會成為引數陣列的項目,並在其中失去其作用。

在這些情況下,您可以使用 extract_options! 對選項雜湊進行特殊處理。此方法會檢查陣列最後一個元素的類型。如果它是雜湊,則會將其彈出並返回,否則會返回一個空雜湊。

讓我們來看一個 caches_action 控制器巨集的定義範例

def caches_action(*actions)
  return unless cache_configured?
  options = actions.extract_options!
  # ...
end

此方法接收任意數量的 action 名稱,以及一個可選的選項雜湊作為最後一個參數。透過呼叫 extract_options!,您可以取得選項雜湊,並以簡單明瞭的方式從 actions 中移除它。

11.4 轉換

11.4.1 to_sentence

方法 to_sentence 將陣列轉換為一個包含句子,並列舉其項目的字串

%w().to_sentence                # => ""
%w(Earth).to_sentence           # => "Earth"
%w(Earth Wind).to_sentence      # => "Earth and Wind"
%w(Earth Wind Fire).to_sentence # => "Earth, Wind, and Fire"

此方法接受三個選項

  • :two_words_connector:用於長度為 2 的陣列的連接詞。預設值為 " and "。
  • :words_connector:用於連接包含 3 個或更多元素的陣列的元素,除了最後兩個元素。預設值為 ", "。
  • :last_word_connector:用於連接包含 3 個或更多元素的陣列的最後兩個元素的連接詞。預設值為 ", and "。

這些選項的預設值可以本地化,它們的鍵是

選項 I18n 鍵
:two_words_connector support.array.two_words_connector
:words_connector support.array.words_connector
:last_word_connector support.array.last_word_connector

11.4.2 to_fs

方法 to_fs 預設行為類似 to_s

但是,如果陣列包含回應 id 的項目,則可以傳遞符號 :db 作為參數。這通常用於 Active Record 物件的集合。返回的字串是

[].to_fs(:db)            # => "null"
[user].to_fs(:db)        # => "8456"
invoice.lines.to_fs(:db) # => "23,567,556,12"

上面範例中的整數應該來自各自對 id 的呼叫。

11.4.3 to_xml

方法 to_xml 返回一個包含接收者 XML 表示形式的字串

Contributor.limit(2).order(:rank).to_xml
# =>
# <?xml version="1.0" encoding="UTF-8"?>
# <contributors type="array">
#   <contributor>
#     <id type="integer">4356</id>
#     <name>Jeremy Kemper</name>
#     <rank type="integer">1</rank>
#     <url-id>jeremy-kemper</url-id>
#   </contributor>
#   <contributor>
#     <id type="integer">4404</id>
#     <name>David Heinemeier Hansson</name>
#     <rank type="integer">2</rank>
#     <url-id>david-heinemeier-hansson</url-id>
#   </contributor>
# </contributors>

為此,它會依序向每個項目發送 to_xml,並在根節點下收集結果。所有項目都必須回應 to_xml,否則會引發例外。

預設情況下,根元素的名稱是第一個項目的類別名稱的底線形式和破折號形式的複數,前提是其餘元素屬於該類型(使用 is_a? 檢查)並且它們不是雜湊。在上面的範例中,它是 "contributors"。

如果有任何元素不屬於第一個元素的類型,則根節點會變成 "objects"

[Contributor.first, Commit.first].to_xml
# =>
# <?xml version="1.0" encoding="UTF-8"?>
# <objects type="array">
#   <object>
#     <id type="integer">4583</id>
#     <name>Aaron Batalion</name>
#     <rank type="integer">53</rank>
#     <url-id>aaron-batalion</url-id>
#   </object>
#   <object>
#     <author>Joshua Peek</author>
#     <authored-timestamp type="datetime">2009-09-02T16:44:36Z</authored-timestamp>
#     <branch>origin/master</branch>
#     <committed-timestamp type="datetime">2009-09-02T16:44:36Z</committed-timestamp>
#     <committer>Joshua Peek</committer>
#     <git-show nil="true"></git-show>
#     <id type="integer">190316</id>
#     <imported-from-svn type="boolean">false</imported-from-svn>
#     <message>Kill AMo observing wrap_with_notifications since ARes was only using it</message>
#     <sha1>723a47bfb3708f968821bc969a9a3fc873a3ed58</sha1>
#   </object>
# </objects>

如果接收者是雜湊陣列,則根元素預設也是 "objects"

[{ a: 1, b: 2 }, { c: 3 }].to_xml
# =>
# <?xml version="1.0" encoding="UTF-8"?>
# <objects type="array">
#   <object>
#     <b type="integer">2</b>
#     <a type="integer">1</a>
#   </object>
#   <object>
#     <c type="integer">3</c>
#   </object>
# </objects>

如果集合為空,則根元素預設為 "nil-classes"。這是一個陷阱,例如,如果集合為空,則上面貢獻者列表的根元素將不會是 "contributors",而是 "nil-classes"。您可以使用 :root 選項來確保根元素的一致性。

子節點的名稱預設是根節點的單數形式。在上面的範例中,我們已經看到 "contributor" 和 "object"。選項 :children 允許您設定這些節點名稱。

預設的 XML 建構器是 Builder::XmlMarkup 的全新實例。您可以使用 :builder 選項配置您自己的建構器。該方法也接受像 :dasherize 之類的選項,它們會被轉發給建構器

Contributor.limit(2).order(:rank).to_xml(skip_types: true)
# =>
# <?xml version="1.0" encoding="UTF-8"?>
# <contributors>
#   <contributor>
#     <id>4356</id>
#     <name>Jeremy Kemper</name>
#     <rank>1</rank>
#     <url-id>jeremy-kemper</url-id>
#   </contributor>
#   <contributor>
#     <id>4404</id>
#     <name>David Heinemeier Hansson</name>
#     <rank>2</rank>
#     <url-id>david-heinemeier-hansson</url-id>
#   </contributor>
# </contributors>

11.5 包裝

方法 Array.wrap 將其參數包裝在陣列中,除非它已經是一個陣列(或類似陣列)。

具體來說

  • 如果參數為 nil,則返回一個空陣列。
  • 否則,如果參數回應 to_ary,則會調用它,如果 to_ary 的值不是 nil,則會返回該值。
  • 否則,返回一個以該參數作為其單一元素的陣列。
Array.wrap(nil)       # => []
Array.wrap([1, 2, 3]) # => [1, 2, 3]
Array.wrap(0)         # => [0]

此方法的目的與 Kernel#Array 相似,但有一些差異

  • 如果參數回應 to_ary,則會調用該方法。如果傳回的值為 nilKernel#Array 會繼續嘗試 to_a,但 Array.wrap 會立即傳回一個以該參數作為其單一元素的陣列。
  • 如果從 to_ary 傳回的值既不是 nil 也不是 Array 物件,Kernel#Array 會引發例外,而 Array.wrap 不會,它只會傳回該值。
  • 它不會對參數呼叫 to_a,如果參數不回應 to_ary,它會返回一個以該參數作為其單一元素的陣列。

最後一點對於某些可列舉物件來說尤其值得比較

Array.wrap(foo: :bar) # => [{:foo=>:bar}]
Array(foo: :bar)      # => [[:foo, :bar]]

還有一個相關的慣用語使用 splat 運算子

[*object]

11.6 複製

方法 Array#deep_dup 使用 Active Support 方法 Object#deep_dup 遞迴地複製自身以及內部所有物件。它的工作方式類似於 Array#map,將 deep_dup 方法發送到內部的每個物件。

array = [1, [2, 3]]
dup = array.deep_dup
dup[1][2] = 4
array[1][2] == nil   # => true

11.7 分組

11.7.1 in_groups_of(number, fill_with = nil)

方法 in_groups_of 將陣列分割成特定大小的連續組。它會返回一個包含這些組的陣列

[1, 2, 3].in_groups_of(2) # => [[1, 2], [3, nil]]

如果傳遞了區塊,則會依序產生這些組

<% sample.in_groups_of(3) do |a, b, c| %>
  <tr>
    <td><%= a %></td>
    <td><%= b %></td>
    <td><%= c %></td>
  </tr>
<% end %>

第一個範例顯示 in_groups_of 如何用盡可能多的 nil 元素填充最後一個組,以達到請求的大小。您可以使用第二個可選參數更改此填充值

[1, 2, 3].in_groups_of(2, 0) # => [[1, 2], [3, 0]]

您可以通過傳遞 false 來告知方法不填充最後一個組

[1, 2, 3].in_groups_of(2, false) # => [[1, 2], [3]]

因此,false 不能用作填充值。

11.7.2 in_groups(number, fill_with = nil)

方法 in_groups 將陣列分割成特定數量的組。該方法會返回一個包含這些組的陣列

%w(1 2 3 4 5 6 7).in_groups(3)
# => [["1", "2", "3"], ["4", "5", nil], ["6", "7", nil]]

如果傳遞了區塊,則會依序產生這些組

%w(1 2 3 4 5 6 7).in_groups(3) { |group| p group }
["1", "2", "3"]
["4", "5", nil]
["6", "7", nil]

上面的範例顯示 in_groups 如何根據需要用尾部的 nil 元素填充某些組。一個組最多可以獲得這些額外元素中的一個,如果有任何一個,則是最右邊的一個。並且具有它們的組始終是最後的組。

您可以使用第二個可選參數更改此填充值

%w(1 2 3 4 5 6 7).in_groups(3, "0")
# => [["1", "2", "3"], ["4", "5", "0"], ["6", "7", "0"]]

您可以通過傳遞 false 來告知方法不填充較小的組

%w(1 2 3 4 5 6 7).in_groups(3, false)
# => [["1", "2", "3"], ["4", "5"], ["6", "7"]]

因此,false 不能用作填充值。

11.7.3 split(value = nil)

方法 split 按分隔符號分割陣列並返回產生的區塊。

如果傳遞了區塊,則分隔符號是陣列中區塊返回 true 的那些元素

(-5..5).to_a.split { |i| i.multiple_of?(4) }
# => [[-5], [-3, -2, -1], [1, 2, 3], [5]]

否則,接收的參數值(預設為 nil)是分隔符號

[0, 1, -5, 1, 1, "foo", "bar"].split(1)
# => [[0], [-5], [], ["foo", "bar"]]

請注意,在前面的範例中,連續的分隔符號會導致空陣列。

12 Hash 的擴充

12.1 轉換

12.1.1 to_xml

方法 to_xml 返回一個包含其接收者 XML 表示形式的字串

{ foo: 1, bar: 2 }.to_xml
# =>
# <?xml version="1.0" encoding="UTF-8"?>
# <hash>
#   <foo type="integer">1</foo>
#   <bar type="integer">2</bar>
# </hash>

為此,該方法會循環遍歷這些對,並建立依賴於的節點。給定一個對 keyvalue

  • 如果 value 是一個雜湊,則會以 key 作為 :root 進行遞迴呼叫。

  • 如果 value 是一個陣列,則會以 key 作為 :root,並以 key 的單數形式作為 :children 進行遞迴呼叫。

  • 如果 value 是一個可呼叫的物件,它必須預期一個或兩個參數。根據 arity,可呼叫物件會以 options 雜湊作為第一個參數,以 key 作為 :root,並以 key 的單數形式作為第二個參數調用。其返回值會成為一個新的節點。

  • 如果 value 回應 to_xml,則會以 key 作為 :root 調用該方法。

  • 否則,會建立一個以 key 作為標記的節點,並以 value 的字串表示形式作為文字節點。如果 valuenil,則會添加一個 "nil" 屬性並設定為 "true"。除非 :skip_types 選項存在且為 true,否則也會根據以下對應添加一個 "type" 屬性

XML_TYPE_NAMES = {
  "Symbol"     => "symbol",
  "Integer"    => "integer",
  "BigDecimal" => "decimal",
  "Float"      => "float",
  "TrueClass"  => "boolean",
  "FalseClass" => "boolean",
  "Date"       => "date",
  "DateTime"   => "datetime",
  "Time"       => "datetime"
}

預設情況下,根節點為 "hash",但可以通過 :root 選項進行配置。

預設的 XML 建構器是 Builder::XmlMarkup 的全新實例。您可以使用 :builder 選項配置您自己的建構器。該方法也接受像 :dasherize 之類的選項,它們會被轉發給建構器。

12.2 合併

Ruby 有一個內建方法 Hash#merge 可以合併兩個雜湊

{ a: 1, b: 1 }.merge(a: 0, c: 2)
# => {:a=>0, :b=>1, :c=>2}

Active Support 定義了一些可能很方便的合併雜湊的更多方法。

12.2.1 reverse_mergereverse_merge!

如果發生衝突,則參數雜湊中的鍵在 merge 中獲勝。您可以使用以下慣用語以緊湊的方式支援帶有預設值的選項雜湊

options = { length: 30, omission: "..." }.merge(options)

如果您更喜歡這種替代表示法,Active Support 會定義 reverse_merge

options = options.reverse_merge(length: 30, omission: "...")

以及一個就地執行合併的驚嘆號版本 reverse_merge!

options.reverse_merge!(length: 30, omission: "...")

請注意,reverse_merge! 可能會更改呼叫者中的雜湊,這可能是一個好主意,也可能不是。

12.2.2 reverse_update

方法 reverse_update 是上面解釋的 reverse_merge! 的別名。

請注意,reverse_update 沒有驚嘆號。

12.2.3 deep_mergedeep_merge!

正如您在前面的範例中看到的那樣,如果在兩個雜湊中都找到了鍵,則參數中的值獲勝。

Active Support 定義 Hash#deep_merge。在深度合併中,如果在兩個雜湊中都找到了一個鍵,並且它們的值又都是雜湊,則它們的合併會成為結果雜湊中的值

{ a: { b: 1 } }.deep_merge(a: { c: 2 })
# => {:a=>{:b=>1, :c=>2}}

方法 deep_merge! 就地執行深度合併。

12.3 深度複製

方法 Hash#deep_dup 使用 Active Support 方法 Object#deep_dup 遞迴地複製自身以及內部所有鍵和值。它的工作方式類似於 Enumerator#each_with_object,將 deep_dup 方法發送到內部的每一對。

hash = { a: 1, b: { c: 2, d: [3, 4] } }

dup = hash.deep_dup
dup[:b][:e] = 5
dup[:b][:d] << 5

hash[:b][:e] == nil      # => true
hash[:b][:d] == [3, 4]   # => true

12.4 使用鍵

12.4.1 except!

方法 except! 與內建的 except 方法相同,但會就地移除鍵,並返回 self

{ a: 1, b: 2 }.except!(:a) # => {:b=>2}
{ a: 1, b: 2 }.except!(:c) # => {:a=>1, :b=>2}

如果接收者響應 convert_key,則會對每個參數調用該方法。這使得 except!(和 except)可以與具有非強制存取的雜湊表很好地協同工作。

{ a: 1 }.with_indifferent_access.except!(:a)  # => {}
{ a: 1 }.with_indifferent_access.except!("a") # => {}

12.4.2 stringify_keysstringify_keys!

方法 stringify_keys 返回一個雜湊表,其中包含接收者中鍵的字串化版本。它通過向它們發送 to_s 來實現。

{ nil => nil, 1 => 1, a: :a }.stringify_keys
# => {"" => nil, "1" => 1, "a" => :a}

如果發生鍵衝突,則值將是最近插入雜湊表中的值。

{ "a" => 1, a: 2 }.stringify_keys
# The result will be
# => {"a"=>2}

此方法對於輕鬆接受符號和字串作為選項可能很有用。例如,ActionView::Helpers::FormHelper 定義了:

def to_checkbox_tag(options = {}, checked_value = "1", unchecked_value = "0")
  options = options.stringify_keys
  options["type"] = "checkbox"
  # ...
end

第二行可以安全地存取 "type" 鍵,並讓使用者傳遞 :type 或 "type"。

還有驚嘆號變體 stringify_keys!,它會就地字串化鍵。

除此之外,可以使用 deep_stringify_keysdeep_stringify_keys! 來字串化給定雜湊表及其所有巢狀雜湊表中的所有鍵。結果範例為:

{ nil => nil, 1 => 1, nested: { a: 3, 5 => 5 } }.deep_stringify_keys
# => {""=>nil, "1"=>1, "nested"=>{"a"=>3, "5"=>5}}

12.4.3 symbolize_keyssymbolize_keys!

方法 symbolize_keys 返回一個雜湊表,其中包含接收者中鍵的符號化版本(如果可能)。它通過向它們發送 to_sym 來實現。

{ nil => nil, 1 => 1, "a" => "a" }.symbolize_keys
# => {nil=>nil, 1=>1, :a=>"a"}

請注意,在前面的範例中,只有一個鍵被符號化。

如果發生鍵衝突,則值將是最近插入雜湊表中的值。

{ "a" => 1, a: 2 }.symbolize_keys
# => {:a=>2}

此方法對於輕鬆接受符號和字串作為選項可能很有用。例如,ActionText::TagHelper 定義了:

def rich_textarea_tag(name, value = nil, options = {})
  options = options.symbolize_keys

  options[:input] ||= "trix_input_#{ActionText::TagHelper.id += 1}"
  # ...
end

第三行可以安全地存取 :input 鍵,並讓使用者傳遞 :input 或 "input"。

還有驚嘆號變體 symbolize_keys!,它會就地符號化鍵。

除此之外,可以使用 deep_symbolize_keysdeep_symbolize_keys! 來符號化給定雜湊表及其所有巢狀雜湊表中的所有鍵。結果範例為:

{ nil => nil, 1 => 1, "nested" => { "a" => 3, 5 => 5 } }.deep_symbolize_keys
# => {nil=>nil, 1=>1, nested:{a:3, 5=>5}}

12.4.4 to_optionsto_options!

方法 to_optionsto_options! 分別是 symbolize_keyssymbolize_keys! 的別名。

12.4.5 assert_valid_keys

方法 assert_valid_keys 接收任意數量的參數,並檢查接收者是否具有該列表之外的任何鍵。如果有,則會引發 ArgumentError

{ a: 1 }.assert_valid_keys(:a)  # passes
{ a: 1 }.assert_valid_keys("a") # ArgumentError

例如,Active Record 在建立關聯時不接受未知的選項。它通過 assert_valid_keys 實現該控制。

12.5 使用值

12.5.1 deep_transform_valuesdeep_transform_values!

方法 deep_transform_values 返回一個新的雜湊表,其中所有值都由區塊操作轉換。這包括根雜湊表和所有巢狀雜湊表和陣列中的值。

hash = { person: { name: "Rob", age: "28" } }

hash.deep_transform_values { |value| value.to_s.upcase }
# => {person: {name: "ROB", age: "28"}}

還有驚嘆號變體 deep_transform_values!,它通過使用區塊操作來破壞性地轉換所有值。

12.6 切片

方法 slice! 會將雜湊表替換為僅包含給定鍵,並返回一個包含已移除的鍵/值對的雜湊表。

hash = { a: 1, b: 2 }
rest = hash.slice!(:a) # => {:b=>2}
hash                   # => {:a=>1}

12.7 提取

方法 extract! 會移除並返回與給定鍵匹配的鍵/值對。

hash = { a: 1, b: 2 }
rest = hash.extract!(:a) # => {:a=>1}
hash                     # => {:b=>2}

方法 extract! 返回的雜湊表子類與接收者相同。

hash = { a: 1, b: 2 }.with_indifferent_access
rest = hash.extract!(:a).class
# => ActiveSupport::HashWithIndifferentAccess

12.8 非強制存取

方法 with_indifferent_access 從其接收者返回一個 ActiveSupport::HashWithIndifferentAccess

{ a: 1 }.with_indifferent_access["a"] # => 1

13 Regexp 的擴展

13.1 multiline?

方法 multiline? 表示正則表達式是否設置了 /m 標誌,也就是說,點是否匹配換行符。

%r{.}.multiline?  # => false
%r{.}m.multiline? # => true

Regexp.new(".").multiline?                    # => false
Regexp.new(".", Regexp::MULTILINE).multiline? # => true

Rails 在單個位置也使用此方法,也在路由程式碼中。路由需求不允許使用多行正則表達式,此標誌有助於強制執行該約束。

def verify_regexp_requirements(requirements)
  # ...
  if requirement.multiline?
    raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
  end
  # ...
end

14 Range 的擴展

14.1 to_fs

Active Support 定義 Range#to_fs 作為 to_s 的替代方案,它理解可選的格式參數。在撰寫本文時,唯一支援的非預設格式是 :db

(Date.today..Date.tomorrow).to_fs
# => "2009-10-25..2009-10-26"

(Date.today..Date.tomorrow).to_fs(:db)
# => "BETWEEN '2009-10-25' AND '2009-10-26'"

如範例所示,:db 格式生成一個 BETWEEN SQL 子句。Active Record 在其對條件中範圍值的支援中使用了該子句。

14.2 ===include?

方法 Range#===Range#include? 表示某些值是否落在給定實例的端點之間

(2..3).include?(Math::E) # => true

Active Support 擴展了這些方法,以便參數可以依次是另一個範圍。在這種情況下,我們測試參數範圍的端點是否屬於接收者本身

(1..10) === (3..7)  # => true
(1..10) === (0..7)  # => false
(1..10) === (3..11) # => false
(1...9) === (3..9)  # => false

(1..10).include?(3..7)  # => true
(1..10).include?(0..7)  # => false
(1..10).include?(3..11) # => false
(1...9).include?(3..9)  # => false

14.3 overlap?

方法 Range#overlap? 表示任意兩個給定範圍是否具有非空交集

(1..10).overlap?(7..11)  # => true
(1..10).overlap?(0..7)   # => true
(1..10).overlap?(11..27) # => false

15 Date 的擴展

15.1 計算

以下計算方法在 1582 年 10 月有邊緣情況,因為 5..14 日根本不存在。本指南為了簡潔起見,不記錄它們在這些日子附近的行為,但足以說明它們會執行您期望的操作。也就是說,Date.new(1582, 10, 4).tomorrow 返回 Date.new(1582, 10, 15),依此類推。請在 Active Support 測試套件中檢查 test/core_ext/date_ext_test.rb 以了解預期行為。

15.1.1 Date.current

Active Support 定義 Date.current 為目前時區的今天。這類似於 Date.today,但會考慮使用者時區(如果已定義)。它還定義了 Date.yesterdayDate.tomorrow,以及實例謂詞 past?today?tomorrow?next_day?yesterday?prev_day?future?on_weekday?on_weekend?,它們都相對於 Date.current

使用考慮使用者時區的方法進行日期比較時,請務必使用 Date.current 而不是 Date.today。在某些情況下,使用者時區可能比系統時區提前,而 Date.today 預設使用系統時區。這表示 Date.today 可能等於 Date.yesterday

15.1.2 具名日期

15.1.2.1 beginning_of_weekend_of_week

方法 beginning_of_weekend_of_week 分別返回一週的開始和結束日期。假設一週從星期一開始,但是可以通過傳遞參數、設定執行緒本地 Date.beginning_of_weekconfig.beginning_of_week 來變更。

d = Date.new(2010, 5, 8)     # => Sat, 08 May 2010
d.beginning_of_week          # => Mon, 03 May 2010
d.beginning_of_week(:sunday) # => Sun, 02 May 2010
d.end_of_week                # => Sun, 09 May 2010
d.end_of_week(:sunday)       # => Sat, 08 May 2010

beginning_of_weekat_beginning_of_week 的別名,而 end_of_weekat_end_of_week 的別名。

15.1.2.2 mondaysunday

方法 mondaysunday 分別返回前一個星期一和下一個星期日的日期。

d = Date.new(2010, 5, 8)     # => Sat, 08 May 2010
d.monday                     # => Mon, 03 May 2010
d.sunday                     # => Sun, 09 May 2010

d = Date.new(2012, 9, 10)    # => Mon, 10 Sep 2012
d.monday                     # => Mon, 10 Sep 2012

d = Date.new(2012, 9, 16)    # => Sun, 16 Sep 2012
d.sunday                     # => Sun, 16 Sep 2012
15.1.2.3 prev_weeknext_week

方法 next_week 接收一個帶有英文日期名稱的符號(預設為執行緒本地 Date.beginning_of_weekconfig.beginning_of_week:monday),並返回對應該日期的日期。

d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
d.next_week              # => Mon, 10 May 2010
d.next_week(:saturday)   # => Sat, 15 May 2010

方法 prev_week 類似。

d.prev_week              # => Mon, 26 Apr 2010
d.prev_week(:saturday)   # => Sat, 01 May 2010
d.prev_week(:friday)     # => Fri, 30 Apr 2010

prev_weeklast_week 的別名。

當設定 Date.beginning_of_weekconfig.beginning_of_week 時,next_weekprev_week 都會按預期工作。

15.1.2.4 beginning_of_monthend_of_month

方法 beginning_of_monthend_of_month 返回月份的開始和結束日期

d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
d.beginning_of_month     # => Sat, 01 May 2010
d.end_of_month           # => Mon, 31 May 2010

beginning_of_monthat_beginning_of_month 的別名,而 end_of_monthat_end_of_month 的別名。

15.1.2.5 quarterbeginning_of_quarterend_of_quarter

方法 quarter 返回接收者日曆年的季度

d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
d.quarter                # => 2

方法 beginning_of_quarterend_of_quarter 返回接收者日曆年的季度的開始和結束日期

d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
d.beginning_of_quarter   # => Thu, 01 Apr 2010
d.end_of_quarter         # => Wed, 30 Jun 2010

beginning_of_quarterat_beginning_of_quarter 的別名,而 end_of_quarterat_end_of_quarter 的別名。

15.1.2.6 beginning_of_yearend_of_year

方法 beginning_of_yearend_of_year 會回傳一年的開始和結束日期。

d = Date.new(2010, 5, 9) # => Sun, 09 May 2010
d.beginning_of_year      # => Fri, 01 Jan 2010
d.end_of_year            # => Fri, 31 Dec 2010

beginning_of_yearat_beginning_of_year 的別名,而 end_of_yearat_end_of_year 的別名。

15.1.3 其他日期計算

15.1.3.1 years_agoyears_since

方法 years_ago 接收一個年份數,並回傳那些年份之前的同一天日期。

date = Date.new(2010, 6, 7)
date.years_ago(10) # => Wed, 07 Jun 2000

years_since 會將日期向前推移。

date = Date.new(2010, 6, 7)
date.years_since(10) # => Sun, 07 Jun 2020

如果該日期不存在,則會回傳該月份的最後一天。

Date.new(2012, 2, 29).years_ago(3)     # => Sat, 28 Feb 2009
Date.new(2012, 2, 29).years_since(3)   # => Sat, 28 Feb 2015

last_year#years_ago(1) 的簡寫。

15.1.3.2 months_agomonths_since

方法 months_agomonths_since 的運作方式與月份類似。

Date.new(2010, 4, 30).months_ago(2)   # => Sun, 28 Feb 2010
Date.new(2010, 4, 30).months_since(2) # => Wed, 30 Jun 2010

如果該日期不存在,則會回傳該月份的最後一天。

Date.new(2010, 4, 30).months_ago(2)    # => Sun, 28 Feb 2010
Date.new(2009, 12, 31).months_since(2) # => Sun, 28 Feb 2010

last_month#months_ago(1) 的簡寫。

15.1.3.3 weeks_agoweeks_since

方法 weeks_ago 和 [weeks_since][DateAndTime::Calculations#week_since] 的運作方式與週類似。

Date.new(2010, 5, 24).weeks_ago(1)   # => Mon, 17 May 2010
Date.new(2010, 5, 24).weeks_since(2) # => Mon, 07 Jun 2010
15.1.3.4 advance

跳轉到其他日期的最通用方式是使用 advance。此方法接收一個帶有鍵 :years:months:weeks:days 的雜湊,並回傳依據指定的鍵值前進後的日期。

date = Date.new(2010, 6, 6)
date.advance(years: 1, weeks: 2)  # => Mon, 20 Jun 2011
date.advance(months: 2, days: -2) # => Wed, 04 Aug 2010

請注意,在先前的範例中,增量可能是負數。

15.1.4 變更元件

方法 change 可讓您取得一個新的日期,該日期與接收者相同,但指定的年、月或日除外。

Date.new(2010, 12, 23).change(year: 2011, month: 11)
# => Wed, 23 Nov 2011

此方法不容許不存在的日期。如果變更無效,則會引發 ArgumentError

Date.new(2010, 1, 31).change(month: 2)
# => ArgumentError: invalid date

15.1.5 持續時間

Duration 物件可以加入日期或從日期中減去。

d = Date.current
# => Mon, 09 Aug 2010
d + 1.year
# => Tue, 09 Aug 2011
d - 3.hours
# => Sun, 08 Aug 2010 21:00:00 UTC +00:00

它們會轉換為對 sinceadvance 的呼叫。例如,在這裡我們可以在日曆改革中獲得正確的跳轉。

Date.new(1582, 10, 4) + 1.day
# => Fri, 15 Oct 1582

15.1.6 時間戳記

如果可能,以下方法會回傳一個 Time 物件,否則會回傳 DateTime。如果已設定,它們會採用使用者時區。

15.1.6.1 beginning_of_dayend_of_day

方法 beginning_of_day 會回傳一天的開始時間戳記 (00:00:00)。

date = Date.new(2010, 6, 7)
date.beginning_of_day # => Mon Jun 07 00:00:00 +0200 2010

方法 end_of_day 會回傳一天的結束時間戳記 (23:59:59)。

date = Date.new(2010, 6, 7)
date.end_of_day # => Mon Jun 07 23:59:59 +0200 2010

beginning_of_dayat_beginning_of_daymidnightat_midnight 的別名。

15.1.6.2 beginning_of_hourend_of_hour

方法 beginning_of_hour 會回傳一小時的開始時間戳記 (hh:00:00)。

date = DateTime.new(2010, 6, 7, 19, 55, 25)
date.beginning_of_hour # => Mon Jun 07 19:00:00 +0200 2010

方法 end_of_hour 會回傳一小時的結束時間戳記 (hh:59:59)。

date = DateTime.new(2010, 6, 7, 19, 55, 25)
date.end_of_hour # => Mon Jun 07 19:59:59 +0200 2010

beginning_of_hourat_beginning_of_hour 的別名。

15.1.6.3 beginning_of_minuteend_of_minute

方法 beginning_of_minute 會回傳一分鐘的開始時間戳記 (hh:mm:00)。

date = DateTime.new(2010, 6, 7, 19, 55, 25)
date.beginning_of_minute # => Mon Jun 07 19:55:00 +0200 2010

方法 end_of_minute 會回傳一分鐘的結束時間戳記 (hh:mm:59)。

date = DateTime.new(2010, 6, 7, 19, 55, 25)
date.end_of_minute # => Mon Jun 07 19:55:59 +0200 2010

beginning_of_minuteat_beginning_of_minute 的別名。

beginning_of_hourend_of_hourbeginning_of_minuteend_of_minute 是針對 TimeDateTime 實作的,但 **不適用於** Date,因為要求 Date 實例的小時或分鐘的開始或結束沒有意義。

15.1.6.4 agosince

方法 ago 接收一個秒數作為引數,並回傳從午夜開始的那些秒數之前時間戳記。

date = Date.current # => Fri, 11 Jun 2010
date.ago(1)         # => Thu, 10 Jun 2010 23:59:59 EDT -04:00

類似地,since 會向前移動。

date = Date.current # => Fri, 11 Jun 2010
date.since(1)       # => Fri, 11 Jun 2010 00:00:01 EDT -04:00

16 DateTime 的擴充功能

DateTime 不知道 DST 規則,因此當 DST 發生變更時,這些方法中的某些方法會有邊緣案例。例如,seconds_since_midnight 可能不會回傳當天的實際數值。

16.1 計算

類別 DateTimeDate 的子類別,因此透過載入 active_support/core_ext/date/calculations.rb,您會繼承這些方法及其別名,但它們始終會回傳日期時間。

以下方法已重新實作,因此您 **不** 需要為這些方法載入 active_support/core_ext/date/calculations.rb

另一方面,advancechange 也會定義,並支援更多選項,它們的說明如下。

以下方法僅在 active_support/core_ext/date_time/calculations.rb 中實作,因為它們僅在與 DateTime 實例一起使用時才有意義。

16.1.1 具名日期時間

16.1.1.1 DateTime.current

Active Support 將 DateTime.current 定義為類似 Time.now.to_datetime,但它會採用使用者時區 (如果已定義)。實例謂詞 past?future? 是相對於 DateTime.current 定義的。

16.1.2 其他擴充功能

16.1.2.1 seconds_since_midnight

方法 seconds_since_midnight 會回傳自午夜以來的秒數。

now = DateTime.current     # => Mon, 07 Jun 2010 20:26:36 +0000
now.seconds_since_midnight # => 73596
16.1.2.2 utc

方法 utc 會以 UTC 表示接收者中的相同日期時間。

now = DateTime.current # => Mon, 07 Jun 2010 19:27:52 -0400
now.utc                # => Mon, 07 Jun 2010 23:27:52 +0000

此方法也別名為 getutc

16.1.2.3 utc?

謂詞 utc? 會指出接收者是否以 UTC 作為其時區。

now = DateTime.now # => Mon, 07 Jun 2010 19:30:47 -0400
now.utc?           # => false
now.utc.utc?       # => true
16.1.2.4 advance

跳轉到另一個日期時間的最通用方式是使用 advance。此方法接收一個帶有鍵 :years:months:weeks:days:hours:minutes:seconds 的雜湊,並回傳依據指定的鍵值前進後的日期時間。

d = DateTime.current
# => Thu, 05 Aug 2010 11:33:31 +0000
d.advance(years: 1, months: 1, days: 1, hours: 1, minutes: 1, seconds: 1)
# => Tue, 06 Sep 2011 12:34:32 +0000

此方法首先會將 :years:months:weeks:days 傳遞給先前說明的文件 Date#advance 來計算目的地日期。之後,它會使用要前進的秒數呼叫 since 來調整時間。此順序很重要,不同的順序會在某些邊緣案例中提供不同的日期時間。Date#advance 中的範例會適用,我們可以將它擴充以顯示與時間位元相關的順序關聯性。

如果我們先移動日期位元(也有處理的相對順序,如先前所述),然後移動時間位元,我們會得到以下計算。

d = DateTime.new(2010, 2, 28, 23, 59, 59)
# => Sun, 28 Feb 2010 23:59:59 +0000
d.advance(months: 1, seconds: 1)
# => Mon, 29 Mar 2010 00:00:00 +0000

但如果我們以另一種方式計算它們,結果會不同。

d.advance(seconds: 1).advance(months: 1)
# => Thu, 01 Apr 2010 00:00:00 +0000

由於 DateTime 不知道 DST,您可能會在沒有警告或錯誤告知您的情況下,最終停留在時間中不存在的點。

16.1.3 變更元件

方法 change 可讓您取得一個新的日期時間,該日期時間與接收者相同,但指定的選項除外,這些選項可能包括 :year:month:day:hour:min:sec:offset:start

now = DateTime.current
# => Tue, 08 Jun 2010 01:56:22 +0000
now.change(year: 2011, offset: Rational(-6, 24))
# => Wed, 08 Jun 2011 01:56:22 -0600

如果小時歸零,則分鐘和秒也會歸零(除非它們有給定的值)。

now.change(hour: 0)
# => Tue, 08 Jun 2010 00:00:00 +0000

同樣地,如果分鐘歸零,則秒也會歸零(除非它已給定一個值)。

now.change(min: 0)
# => Tue, 08 Jun 2010 01:00:00 +0000

此方法不容許不存在的日期。如果變更無效,則會引發 ArgumentError

DateTime.current.change(month: 2, day: 30)
# => ArgumentError: invalid date

16.1.4 持續時間

Duration 物件可以加入日期時間或從日期時間中減去。

now = DateTime.current
# => Mon, 09 Aug 2010 23:15:17 +0000
now + 1.year
# => Tue, 09 Aug 2011 23:15:17 +0000
now - 1.week
# => Mon, 02 Aug 2010 23:15:17 +0000

它們會轉換為對 sinceadvance 的呼叫。例如,在這裡我們可以在日曆改革中獲得正確的跳轉。

DateTime.new(1582, 10, 4, 23) + 1.hour
# => Fri, 15 Oct 1582 00:00:00 +0000

17 Time 的擴充功能

17.1 計算

它們是類似的。請參閱上面它們的文件,並考慮以下差異:

  • change 接受額外的 :usec 選項。
  • Time 了解 DST,因此您會獲得正確的 DST 計算,如下所示。
Time.zone_default
# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>

# In Barcelona, 2010/03/28 02:00 +0100 becomes 2010/03/28 03:00 +0200 due to DST.
t = Time.local(2010, 3, 28, 1, 59, 59)
# => Sun Mar 28 01:59:59 +0100 2010
t.advance(seconds: 1)
# => Sun Mar 28 03:00:00 +0200 2010
  • 如果 sinceago 跳轉到無法使用 Time 表示的時間,則會改為回傳 DateTime 物件。

17.1.1 Time.current

Active Support 定義 Time.current 為目前時區的今天。它類似於 Time.now,但會考慮使用者時區(如果已定義)。它還定義了實例謂詞 past?today?tomorrow?next_day?yesterday?prev_day?future?,所有這些都相對於 Time.current

在使用會考慮使用者時區的方法進行時間比較時,請務必使用 Time.current 而不是 Time.now。在某些情況下,使用者時區相較於系統時區(Time.now 預設使用)可能會在未來。這表示 Time.now.to_date 可能會等於 Date.yesterday

17.1.2 all_dayall_weekall_monthall_quarterall_year

方法 all_day 會傳回一個代表目前時間整天的範圍。

now = Time.current
# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
now.all_day
# => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Mon, 09 Aug 2010 23:59:59 UTC +00:00

類似地,all_weekall_monthall_quarterall_year 的用途都是產生時間範圍。

now = Time.current
# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
now.all_week
# => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Sun, 15 Aug 2010 23:59:59 UTC +00:00
now.all_week(:sunday)
# => Sun, 16 Sep 2012 00:00:00 UTC +00:00..Sat, 22 Sep 2012 23:59:59 UTC +00:00
now.all_month
# => Sat, 01 Aug 2010 00:00:00 UTC +00:00..Tue, 31 Aug 2010 23:59:59 UTC +00:00
now.all_quarter
# => Thu, 01 Jul 2010 00:00:00 UTC +00:00..Thu, 30 Sep 2010 23:59:59 UTC +00:00
now.all_year
# => Fri, 01 Jan 2010 00:00:00 UTC +00:00..Fri, 31 Dec 2010 23:59:59 UTC +00:00

17.1.3 prev_daynext_day

prev_daynext_day 會傳回前一天或後一天的時間。

t = Time.new(2010, 5, 8) # => 2010-05-08 00:00:00 +0900
t.prev_day               # => 2010-05-07 00:00:00 +0900
t.next_day               # => 2010-05-09 00:00:00 +0900

17.1.4 prev_monthnext_month

prev_monthnext_month 會傳回上個月或下個月的同一天時間。

t = Time.new(2010, 5, 8) # => 2010-05-08 00:00:00 +0900
t.prev_month             # => 2010-04-08 00:00:00 +0900
t.next_month             # => 2010-06-08 00:00:00 +0900

如果該日期不存在,則會回傳該月份的最後一天。

Time.new(2000, 5, 31).prev_month # => 2000-04-30 00:00:00 +0900
Time.new(2000, 3, 31).prev_month # => 2000-02-29 00:00:00 +0900
Time.new(2000, 5, 31).next_month # => 2000-06-30 00:00:00 +0900
Time.new(2000, 1, 31).next_month # => 2000-02-29 00:00:00 +0900

17.1.5 prev_yearnext_year

prev_yearnext_year 會傳回去年或明年的同一天/月份時間。

t = Time.new(2010, 5, 8) # => 2010-05-08 00:00:00 +0900
t.prev_year              # => 2009-05-08 00:00:00 +0900
t.next_year              # => 2011-05-08 00:00:00 +0900

如果日期是閏年的 2 月 29 日,則會取得 2 月 28 日。

t = Time.new(2000, 2, 29) # => 2000-02-29 00:00:00 +0900
t.prev_year               # => 1999-02-28 00:00:00 +0900
t.next_year               # => 2001-02-28 00:00:00 +0900

17.1.6 prev_quarternext_quarter

prev_quarternext_quarter 會傳回上季或下季同一天的日期。

t = Time.local(2010, 5, 8) # => 2010-05-08 00:00:00 +0300
t.prev_quarter             # => 2010-02-08 00:00:00 +0200
t.next_quarter             # => 2010-08-08 00:00:00 +0300

如果該日期不存在,則會回傳該月份的最後一天。

Time.local(2000, 7, 31).prev_quarter  # => 2000-04-30 00:00:00 +0300
Time.local(2000, 5, 31).prev_quarter  # => 2000-02-29 00:00:00 +0200
Time.local(2000, 10, 31).prev_quarter # => 2000-07-31 00:00:00 +0300
Time.local(2000, 11, 31).next_quarter # => 2001-03-01 00:00:00 +0200

prev_quarterlast_quarter 的別名。

17.2 時間建構子

Active Support 定義 Time.currentTime.zone.now (如果已定義使用者時區),否則回退到 Time.now

Time.zone_default
# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>
Time.current
# => Fri, 06 Aug 2010 17:11:58 CEST +02:00

DateTime 類似,謂詞 past?future? 相對於 Time.current

如果要建構的時間超出執行平台中 Time 支援的範圍,則會捨棄 usecs,並改為傳回 DateTime 物件。

17.2.1 持續時間

Duration 物件可以加到和減去時間物件。

now = Time.current
# => Mon, 09 Aug 2010 23:20:05 UTC +00:00
now + 1.year
# => Tue, 09 Aug 2011 23:21:11 UTC +00:00
now - 1.week
# => Mon, 02 Aug 2010 23:21:11 UTC +00:00

它們會轉換為對 sinceadvance 的呼叫。例如,在這裡我們可以在日曆改革中獲得正確的跳轉。

Time.utc(1582, 10, 3) + 5.days
# => Mon Oct 18 00:00:00 UTC 1582

18 File 的擴充功能

18.1 atomic_write

使用類別方法 File.atomic_write,您可以寫入檔案,以防止任何讀取器看到寫入一半的內容。

檔案的名稱會作為引數傳遞,此方法會產生一個開啟以供寫入的檔案處理程序。區塊完成後,atomic_write 會關閉檔案處理程序並完成其工作。

例如,Action Pack 會使用此方法來寫入像 all.css 的資源快取檔案。

File.atomic_write(joined_asset_path) do |cache|
  cache.write(join_asset_file_contents(asset_paths))
end

為了完成此作業,atomic_write 會建立一個暫存檔案。區塊中的程式碼實際寫入該檔案。完成後,會重新命名暫存檔案,這是在 POSIX 系統上的原子操作。如果目標檔案存在,atomic_write 會覆寫它並保留擁有者和權限。但是,在某些情況下,atomic_write 無法變更檔案擁有權或權限,此錯誤會被捕捉並跳過,信任使用者/檔案系統來確保需要它的進程可以存取該檔案。

由於 atomic_write 執行的 chmod 作業,如果目標檔案已設定 ACL,則此 ACL 將會重新計算/修改。

請注意,您無法使用 atomic_write 附加內容。

輔助檔案會寫入暫存檔案的標準目錄,但您可以將您選擇的目錄作為第二個引數傳遞。

19 NameError 的擴充功能

Active Support 將 missing_name? 新增至 NameError,以測試是否因作為引數傳遞的名稱而引發例外。

名稱可以作為符號或字串給定。符號會針對裸常數名稱進行測試,字串則針對完整限定常數名稱進行測試。

符號可以表示完整限定的常數名稱,例如 :"ActiveRecord::Base",因此符號的行為是為了方便起見而定義,而不是因為技術上必須如此。

例如,當呼叫 ArticlesController 的動作時,Rails 會樂觀地嘗試使用 ArticlesHelper。Helper 模組不存在是沒有問題的,因此如果針對該常數名稱引發例外,則應該將其靜音。但有可能 articles_helper.rb 因為實際未知的常數而引發 NameError。應該重新引發該錯誤。missing_name? 方法提供了一種區分這兩種情況的方式。

def default_helper_module!
  module_name = name.delete_suffix("Controller")
  module_path = module_name.underscore
  helper module_path
rescue LoadError => e
  raise e unless e.is_missing? "helpers/#{module_path}_helper"
rescue NameError => e
  raise e unless e.missing_name? "#{module_name}Helper"
end

20 LoadError 的擴充功能

Active Support 將 is_missing? 新增至 LoadError

給定路徑名稱,is_missing? 會測試是否因該特定檔案(可能除了 ".rb" 副檔名)而引發例外。

例如,當呼叫 ArticlesController 的動作時,Rails 會嘗試載入 articles_helper.rb,但該檔案可能不存在。這沒關係,Helper 模組並非強制性,因此 Rails 會將載入錯誤靜音。但有可能 Helper 模組確實存在,並且反過來需要另一個遺失的程式庫。在這種情況下,Rails 必須重新引發例外。is_missing? 方法提供了一種區分這兩種情況的方式。

def default_helper_module!
  module_name = name.delete_suffix("Controller")
  module_path = module_name.underscore
  helper module_path
rescue LoadError => e
  raise e unless e.is_missing? "helpers/#{module_path}_helper"
rescue NameError => e
  raise e unless e.missing_name? "#{module_name}Helper"
end

21 Pathname 的擴充功能

21.1 existence

如果具名檔案存在,existence 方法會傳回接收器,否則傳回 nil。這對於像這樣的慣用語很有用:

content = Pathname.new("file").existence&.read


回到頂端