更多内容请访问 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 的扩展。一般来说,对 SomeClass 的扩展可以通过加载 active_support/core_ext/some_class 来一次性加载。

因此,要加载所有对 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

Ruby on Rails 应用程序会加载所有 Active Support,除非 config.active_support.bare 为 true。在这种情况下,应用程序只会加载框架本身为自身需求选择的扩展,并且仍然可以像上一节所述那样以任何粒度级别选择性加载自身。

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 方法返回给定对象的深拷贝。通常,当你复制包含其他对象的某个对象时,Ruby 不会复制它们,因此它会创建一个对象的浅拷贝。例如,如果你有一个包含字符串的数组,它看起来像这样

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"

此方法对需要编码的内容进行编码,包括键和值。

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

因此,它的输出已准备好用于查询字符串。

数组返回对每个元素应用 to_query 并使用 key[] 作为键的结果,并将结果用 "&" 连接起来。

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

哈希也响应 to_query,但签名不同。如果没有传递参数,则调用会生成一系列排序的键值对,对它的值调用 to_query(key)。然后它用 "&" 连接结果。

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

Hash#to_query 方法接受键的可选命名空间。

{ 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 支持

Active Support 提供了比 json gem 通常为 Ruby 对象提供的更好的 to_json 实现。这是因为一些类,比如 HashProcess::Status,需要特殊处理才能提供正确的 JSON 表示。

2.12 实例变量

Active Support 提供了几个方法来简化对实例变量的访问。

2.12.1 instance_values

instance_values 方法返回一个哈希,它将没有“@”的实例变量名称映射到它们对应的值。键是字符串。

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

3Module 的扩展

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 进行配置,你可以传入任何类似 sprintf 的格式字符串,并在字符串开头添加 @,并在某个位置添加 %s,这就是放置名称的位置。默认值为 "@_%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 日志记录器的 API 使用 mattr_accessor 生成

module ActiveStorage
  mattr_accessor :logger
end

3.2 父级

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 匿名

模块可能存在或不存在名称。

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 方法委托

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 重新定义方法

在某些情况下,您需要使用 define_method 定义方法,但不知道是否已经存在具有该名称的方法。如果存在,则在启用警告的情况下会发出警告。没什么大不了的,但也不干净。

方法 redefine_method 可以防止此类潜在的警告,如果需要,会在定义之前删除现有的方法。

您还可以使用 silence_redefinition_of_method,如果您需要自己定义替换方法(例如,因为您正在使用 delegate)。

4Class 的扩展

4.1 类属性

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,而不是任何假值。

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 子类和后代

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]

这些类返回的顺序未指定。

5String 的扩展

5.1 输出安全

5.1.1 动机

将数据插入 HTML 模板需要格外小心。例如,您不能直接将 @review.title 插入 HTML 页面中。首先,如果评论标题是“Flanagan & Matz rules!”,则输出将不是格式良好的,因为必须将“&”转义为“&amp;”。更重要的是,根据应用程序的不同,这可能是一个很大的安全漏洞,因为用户可以通过设置手工制作的评论标题来注入恶意 HTML。有关风险的更多信息,请查看 安全指南 中有关跨站点脚本攻击的部分。

5.1.2 安全字符串

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 转换

根据经验,除了上面解释的串联之外,任何可能更改字符串的方法都会为您提供不安全的字符串。这些是 downcasegsubstripchompunderscore 等。

对于像 gsub! 这样的就地转换,接收者本身会变得不安全。

安全位始终会丢失,无论转换是否实际更改了内容。

5.1.4 转换和强制转换

在安全字符串上调用 to_s 会返回安全字符串,但使用 to_str 进行强制转换会返回不安全字符串。

5.1.5 复制

在安全字符串上调用 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 通常是一个空格或制表符,但它可以是任何字符串。

第三个参数 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 获取控制器类的 lowercase 名称。

# 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,因为它也会将类名取消模块化并检查一些可能影响返回字符串的选项。

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 在相同位置对真实常量进行求值时所做的事情。

邮件测试用例使用 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 从类名中获取外键列名。为此,它会将类名进行去模块化、添加下划线并添加 "_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_date, to_time, to_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

6Symbol 的扩展

6.1 starts_with?ends_with?

Active Support 定义了 Symbol#start_with?Symbol#end_with? 的第三人称别名。

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

7Numeric 的扩展

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"

8Integer 的扩展

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 的时间扩展。

9BigDecimal 的扩展

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"

10Enumerable 的扩展

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

11Array 的扩展

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 返回一个不包含指定元素的 Array 副本。这是一种对 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

此方法接收任意数量的动作名称,以及作为最后一个参数的可选选项哈希。使用对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,则调用此方法。Kernel#Array 继续尝试to_a,如果返回值是nil,但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 是一个可调用对象,则它必须期望一个或两个参数。根据元数,可调用对象将使用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_updatereverse_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

13Regexp 的扩展

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

14Range 的扩展

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

15Date 的扩展

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_week 的别名是 at_beginning_of_weekend_of_week 的别名是 at_end_of_week

15.1.2.2 monday, sunday

方法 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_week, next_week

方法 next_week 接收一个英文星期名称的符号(默认是线程本地 Date.beginning_of_week,或 config.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_week 被别名为 last_week

Date.beginning_of_weekconfig.beginning_of_week 设置时,next_weekprev_week 都能按预期工作。

15.1.2.4 beginning_of_month, end_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_month 被别名为 at_beginning_of_monthend_of_month 被别名为 at_end_of_month

15.1.2.5 quarter, beginning_of_quarter, end_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_quarter 被别名为 at_beginning_of_quarterend_of_quarter 被别名为 at_end_of_quarter

15.1.2.6 beginning_of_year, end_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_year 被别名为 at_beginning_of_yearend_of_year 被别名为 at_end_of_year

15.1.3 其他日期计算

15.1.3.1 years_ago, years_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_ago, months_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_ago, weeks_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_day, end_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_day 被别名为 at_beginning_of_daymidnightat_midnight

15.1.6.2 beginning_of_hour, end_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_hour 被别名为 at_beginning_of_hour

15.1.6.3 beginning_of_minute, end_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_minute 被别名为 at_beginning_of_minute

beginning_of_hourend_of_hourbeginning_of_minuteend_of_minuteTimeDateTime 实现,但 **不** 为 Date 实现,因为在 Date 实例上请求小时或分钟的开始或结束没有意义。

15.1.6.4 ago, since

方法 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 变化发生时,某些方法会出现边缘情况。例如,当 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

此方法首先使用上面文档中提到的 `Date#advance` 方法,通过传递 `:years`、`:months`、`:weeks` 和 `:days` 参数来计算目标日期。之后,使用 `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` 不支持夏令时,因此您可能会遇到时间点不存在的情况,而不会有任何警告或错误提示。

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 支持夏令时,因此您会得到与以下示例中一样的正确的夏令时计算结果
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
  • 如果 `since``ago` 跳转到 `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_day`、`all_week`、`all_month`、`all_quarter` 和 `all_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_week``all_month``all_quarter``all_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_day`、`next_day`

`prev_day``next_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_month`、`next_month`

`prev_month``next_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_year`、`next_year`

`prev_year``next_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_quarter`、`next_quarter`

`prev_quarter``next_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_quarter`last_quarter` 的别名。

17.2 时间构造函数

Active Support 定义了 `Time.current`,如果定义了用户时区,则它等于 `Time.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 向 `NameError` 添加了 `missing_name?`,它测试异常是否因为作为参数传递的名称而引发。

名称可以作为符号或字符串给出。符号将与裸常量名进行比较,字符串将与完全限定的常量名进行比较。

符号可以表示完全限定的常量名,例如 `:"ActiveRecord::Base"`,因此符号的行为是为了方便而定义的,而不是因为它在技术上必须那样。

例如,当调用 `ArticlesController` 的操作时,Rails 会尝试使用 `ArticlesHelper`。如果帮助器模块不存在,这是可以的,因此如果为该常量名引发的异常应该被忽略。但它可能是 `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 向 `LoadError` 添加了 `is_missing?`

给定一个路径名,`is_missing?` 会测试异常是否因为该特定文件(可能除了“.rb”扩展名之外)而引发。

例如,当调用 `ArticlesController` 的操作时,Rails 会尝试加载 `articles_helper.rb`,但该文件可能不存在。这是可以的,帮助器模块不是必需的,因此 Rails 会忽略加载错误。但它可能是帮助器模块存在并依次要求另一个丢失的库。在这种情况下,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


返回顶部