edge
更多信息请访问 rubyonrails.org: 更多 Ruby on Rails

Active Support核心扩展

Active Support是Ruby on Rails组件,负责提供Ruby语言的扩展和实用工具。

它在语言层面上提供了更丰富的功能,既适用于开发Rails应用程序,也适用于开发Ruby on Rails本身。

阅读本指南后,您将了解:

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,说明如下:

注意:定义在active_support/core_ext/hash/indifferent_access.rb中。

这意味着您可以这样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!
  return if self["Cache-Control"].present?
  # ...
end

注意:定义在active_support/core_ext/object/blank.rb中。

2.2 presence

presence方法如果present?则返回接收者,否则返回nil。它对于像这样的习语非常有用: ruby host = config[:host].presence || 'localhost'

注意:定义在active_support/core_ext/object/blank.rb中。

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 (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要快得多。只有在您知道硬编码列表在您的用例中足够时才使用它。

注意:定义在active_support/core_ext/object/duplicable.rb中。

2.4 deep_dup

deep_dup方法返回给定对象的深拷贝。通常,当您对包含其他对象的对象进行dup时,Ruby不会对它们进行dup,因此它创建了对象的浅拷贝。例如,如果您有一个包含字符串的数组,它将如下所示:

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

duplicate.push 'another-string'

# 对象已复制,因此元素仅添加到副本中
array     # => ['string']
duplicate # => ['string', 'another-string']

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

# 第一个元素没有复制,它将在两个数组中都被更改
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

注意:定义在active_support/core_ext/object/deep_dup.rb中。

2.5 try

当您只想在对象不为nil时调用方法时,最简单的方法是使用条件语句,但会增加不必要的混乱。另一种选择是使用trytry类似于Object#public_send,但如果发送给nil,它会返回nil

这是一个例子:

# 不使用try
unless @number.nil?
  @number.next
end

# 使用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

注意:定义在active_support/core_ext/object/try.rb中。

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

注意:定义在active_support/core_ext/kernel/singleton_class.rb中。

2.7 acts_like?(duck)

方法acts_like?提供了一种检查某个类是否像另一个类一样的方法,基于一个简单的约定:提供与String相同接口的类定义了一个acts_like?方法。 ruby def acts_like_string? end

这只是一个标记,它的主体或返回值都是无关紧要的。然后,客户端代码可以通过以下方式查询鸭子类型的安全性:

some_klass.acts_like?(:string)

Rails有一些类似于DateTime的类,并遵循这个约定。

注意:定义在active_support/core_ext/object/acts_like.rb中。

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]的值。

注意:定义在active_support/core_ext/object/to_param.rb中。

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"

因此,它的输出已经准备好在查询字符串中使用。

数组返回将key[]作为键应用to_query到每个元素的结果,并使用“&”连接结果:

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

注意:定义在active_support/core_ext/object/to_query.rb中。

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将调用转发到其接收者,它们可以嵌套。每个嵌套级别都将合并继承的默认值以及它们自己的默认值。

注意:定义在active_support/core_ext/object/with_options.rb中。

2.11 JSON支持

Active Support提供了比json gem在Ruby对象上通常提供的更好的to_json实现。这是因为某些类,如HashProcess::Status,需要特殊处理才能提供正确的JSON表示。 注意:在active_support/core_ext/object/json.rb中定义。

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}

注意:在active_support/core_ext/object/instance_variables.rb中定义。

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

注意:在active_support/core_ext/object/instance_variables.rb中定义。

2.13 消除警告和异常

方法silence_warningsenable_warnings根据其块的持续时间更改$VERBOSE的值,并在之后重置它:

silence_warnings { Object.const_set "RAILS_DEFAULT_LOGGER", logger }

使用suppress也可以消除异常。该方法接收任意数量的异常类。如果在块的执行过程中引发了一个异常,并且该异常是参数中的任何一个的kind_of?suppress会捕获它并返回静默。否则,异常不会被捕获:

# 如果用户被锁定,增量将丢失,没关系。
suppress(ActiveRecord::StaleObjectError) do
  current_user.increment! :visits
end

注意:在active_support/core_ext/kernel/reporting.rb中定义。

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

注意:在active_support/core_ext/object/inclusion.rb中定义。

3 Module的扩展

3.1 属性

3.1.1 alias_attribute

模型属性具有读取器、写入器和谓词。您可以使用alias_attribute为模型属性创建别名,这样对应的三个方法都会为您定义。与其他别名方法一样,新名称是第一个参数,旧名称是第二个参数(一个助记符是它们按照赋值的顺序排列):

class User < ApplicationRecord
  # 您可以将email列称为"login"。
  # 这对于身份验证代码可能是有意义的。
  alias_attribute :login, :email
end

注意:在active_support/core_ext/module/aliasing.rb中定义。

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可以进行配置,您可以传递任何带有前导@和某处的%ssprintf格式字符串,其中将放置名称。默认值为"@_%s"

Rails在一些地方使用内部属性,例如视图:

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

注意:在active_support/core_ext/module/attr_internal.rb中定义。

3.1.3 模块属性

mattr_readermattr_writermattr_accessor与为类定义的cattr_*宏相同。实际上,cattr_*宏只是mattr_*宏的别名。请参阅类属性。 例如,Active Storage的日志记录器的API是使用mattr_accessor生成的:

module ActiveStorage
  mattr_accessor :logger
end

注意:定义在active_support/core_ext/module/attribute_accessors.rb中。

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

注意:定义在active_support/core_ext/module/introspection.rb中。

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

注意:定义在active_support/core_ext/module/introspection.rb中。

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]

注意:定义在active_support/core_ext/module/introspection.rb中。

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

尽管匿名模块在定义上是无法访问的。

注意:定义在active_support/core_ext/module/anonymous.rb中。

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选项应该变成一个表达式,该表达式求值为方法委托的对象。通常是一个字符串或符号。这样的表达式在接收器的上下文中求值:

# 委托给Rails常量
delegate :logger, to: :Rails

# 委托给接收器的类
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 来改变这一点。

注意:定义在 active_support/core_ext/module/delegation.rb

3.4.2 delegate_missing_to

想象一下,您希望将 User 对象上缺失的所有内容委托给 Profile 对象。delegate_missing_to 宏可以让您轻松实现这一点:

class User < ApplicationRecord
  has_one :profile

  delegate_missing_to :profile
end

目标可以是对象内的任何可调用项,例如实例变量、方法、常量等。只有目标的公共方法会被委托。

注意:定义在 active_support/core_ext/module/delegation.rb 中。

3.5 重新定义方法

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

方法 redefine_method 可以防止这种潜在的警告,在需要时删除现有方法。

如果需要自己定义替换方法(例如使用 delegate),还可以使用 silence_redefinition_of_method

注意:定义在 active_support/core_ext/module/redefine_method.rb 中。

4 Class 的扩展

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,来自 A
a2.x # => 2,在 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,它将不会被定义。

注意:定义在 active_support/core_ext/class/attribute.rb 中。

4.1.2 cattr_readercattr_writercattr_accessor

cattr_readercattr_writercattr_accessor 类似于它们的 attr_* 对应物,但用于类。它们将类变量初始化为 nil,除非它已经存在,并生成相应的类方法来访问它:

class MysqlAdapter < AbstractAdapter
  # 生成访问 @@emulate_booleans 的类方法。
  cattr_accessor :emulate_booleans
end

此外,您可以将一个块传递给 cattr_*,以使用默认值设置属性:

class MysqlAdapter < AbstractAdapter
  # 生成访问 @@emulate_booleans 的类方法,并将默认值设置为 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
    # 不会生成first_name实例读取器。
    cattr_accessor :first_name, instance_reader: false
    # 不会生成last_name=实例写入器。
    cattr_accessor :last_name, instance_writer: false
    # 不会生成surname实例读取器或surname=写入器。
    cattr_accessor :surname, instance_accessor: false
  end
end

模型可能会发现将:instance_accessor设置为false是一种防止批量赋值设置属性的方法。

注意:定义在active_support/core_ext/module/attribute_accessors.rb中。

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]

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

注意:定义在active_support/core_ext/class/subclasses.rb中。

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]

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

注意:定义在active_support/core_ext/class/subclasses.rb中。

5 String的扩展

5.1 输出安全性

5.1.1 动机

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

5.1.2 安全字符串

Active Support引入了(html) safe字符串的概念。安全字符串是一种被标记为可以直接插入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 %> <%# 如果需要,会被转义 %>

要直接插入内容,请使用raw助手而不是调用html_safe

<%= raw @cms.current_template %> <%# 将 @cms.current_template 原样插入 %>

或者等效地使用<%==

<%== @cms.current_template %> <%# 将 @cms.current_template 原样插入 %>

raw助手会为您调用html_safe

def raw(stringish)
  stringish.to_s.html_safe
end

注意:定义在active_support/core_ext/string/output_safety.rb中。

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!

注意:定义在active_support/core_ext/string/filters.rb中。

5.3 squish

方法squish去除前导和尾随空格,并将连续的空格替换为一个空格:

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

还有一个破坏性版本String#squish!

注意,它处理ASCII和Unicode空格。

注意:定义在active_support/core_ext/string/filters.rb中。

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阻止了截断。

注意:定义在active_support/core_ext/string/filters.rb中。

5.5 truncate_bytes

方法truncate_bytes返回截断后的副本,最多截断到bytesize字节:

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

省略号可以使用:omission选项自定义:

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

注意:定义在active_support/core_ext/string/filters.rb中。

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!..."

注意:定义在active_support/core_ext/string/filters.rb中。

5.7 inquiry

inquiry方法将字符串转换为StringInquirer对象,使得相等性检查更加美观。

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

注意:定义在active_support/core_ext/string/inquiry.rb中。

5.8 starts_with?ends_with?

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

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

注意:在active_support/core_ext/string/starts_ends_with.rb中定义。

5.9 strip_heredoc

方法strip_heredoc用于去除heredocs中的缩进。

例如,在以下代码中:

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

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

用户将看到对齐在左边边缘的使用消息。

从技术上讲,它会查找整个字符串中缩进最少的行,并删除相应数量的前导空格。

注意:在active_support/core_ext/string/strip.rb中定义。

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!方法在原地执行缩进。

注意:在active_support/core_ext/string/indent.rb中定义。

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

注意:在active_support/core_ext/string/access.rb中定义。

5.11.2 from(position)

from方法返回从position位置开始的字符串的子串:

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

注意:在active_support/core_ext/string/access.rb中定义。

5.11.3 to(position)

to方法返回字符串到position位置的子串:

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

注意:在active_support/core_ext/string/access.rb中定义。

5.11.4 first(limit = 1)

first方法返回包含字符串前limit个字符的子串。

调用str.first(n)等效于str.to(n-1),如果n>0,则返回空字符串n==0。

注意:在active_support/core_ext/string/access.rb中定义。

5.11.5 last(limit = 1)

last方法返回包含字符串后limit个字符的子串。

调用str.last(n)等效于str.from(-n),如果n>0,则返回空字符串n==0。

注意:在active_support/core_ext/string/access.rb中定义。

5.12 Inflections

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

注意:在active_support/core_ext/string/inflections.rb中定义。

5.12.2 singularize

singularize方法是pluralize的反义词:

"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

注意:定义在active_support/core_ext/string/inflections.rb中。

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。

INFO:可以将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

注意:定义在active_support/core_ext/string/inflections.rb中。

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]中获取的值。

INFO:可以将underscore视为camelize的反向操作,但也有一些例外情况。例如,"SSLError".underscore.camelize返回"SslError"

注意:定义在active_support/core_ext/string/inflections.rb中。

5.12.5 titleize

方法titleize将接收者中的单词首字母大写:

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

titleize的别名是titlecase

注意:定义在active_support/core_ext/string/inflections.rb中。

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

注意:定义在active_support/core_ext/string/inflections.rb中。

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

注意:定义在active_support/core_ext/string/inflections.rb中。

5.12.8 deconstantize

给定一个带有限定常量引用表达式的字符串,deconstantize删除最右边的部分,通常只保留常量的容器名称:

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

注意:定义在active_support/core_ext/string/inflections.rb中。

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参数。 ruby "Employee Salary".downcase_first # => "employee Salary" "".downcase_first # => ""

注意:定义在 active_support/core_ext/numeric/conversions.rb

6 Integer 的扩展

6.1 multiple_of?

方法 multiple_of? 用于测试一个整数是否是参数的倍数:

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

注意:定义在 active_support/core_ext/integer/multiple.rb

6.2 ordinal

方法 ordinal 返回与接收整数对应的序数后缀字符串:

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

注意:定义在 active_support/core_ext/integer/inflections.rb

6.3 ordinalize

方法 ordinalize 返回与接收整数对应的序数字符串。相比之下,注意 ordinal 方法只返回后缀字符串。

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

注意:定义在 active_support/core_ext/integer/inflections.rb

6.4 时间

以下方法:

可以用于时间声明和计算,比如 4.months + 5.years。它们的返回值也可以加减时间对象。

这些方法可以与 from_nowago 等方法结合使用,进行精确的日期计算。例如:

# 等同于 Time.current.advance(months: 1)
1.month.from_now

# 等同于 Time.current.advance(years: 2)
2.years.from_now

# 等同于 Time.current.advance(months: 4, years: 5)
(4.months + 5.years).from_now

警告:对于其他持续时间,请参考对 Numeric 的时间扩展。

注意:定义在 active_support/core_ext/integer/time.rb

7 BigDecimal 的扩展

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

8 Enumerable 的扩展

8.1 sum

方法 sum 对可枚举对象的元素进行求和:

[1, 2, 3].sum # => 6
(1..100).sum  # => 5050

求和假设元素可以响应 +

[[1, 2], [2, 3], [3, 4]].sum    # => [1, 2, 2, 3, 3, 4]
%w(foo bar baz).sum             # => "foobarbaz"
{ a: 1, b: 2, c: 3 }.sum          # => [:a, 1, :b, 2, :c, 3]

默认情况下,空集合的和为零,但可以自定义:

[].sum    # => 0
[].sum(1) # => 1

如果给定了一个块,sum 将成为一个迭代器,遍历集合的元素并求和返回的值:

(1..5).sum { |n| n * 2 } # => 30
[2, 4, 6, 8, 10].sum    # => 30

空接收者的和也可以以这种形式自定义:

[].sum(1) { |n| n**3 } # => 1

注意:定义在 active_support/core_ext/enumerable.rb

8.2 index_by

方法 index_by 生成一个由可枚举对象的元素按某个键索引的哈希表。

它遍历集合并将每个元素传递给块。元素将以块返回的值作为键:

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

警告:键通常应该是唯一的。如果块对不同的元素返回相同的值,则不会为该键构建集合。最后一个元素将获胜。

注意:定义在 active_support/core_ext/enumerable.rb

8.3 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 ], … }

注意:在active_support/core_ext/enumerable.rb中定义。

8.4 many?

方法many?collection.size > 1的简写形式:

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

如果给定了可选的块,则many?只考虑返回true的元素:

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

注意:在active_support/core_ext/enumerable.rb中定义。

8.5 exclude?

谓词exclude?测试给定对象是否不属于集合。它是内置include?的否定形式:

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

注意:在active_support/core_ext/enumerable.rb中定义。

8.6 including

方法including返回一个包含传入元素的新的可枚举对象:

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

注意:在active_support/core_ext/enumerable.rb中定义。

8.7 excluding

方法excluding返回一个移除了指定元素的可枚举对象的副本:

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

excludingwithout的别名。

注意:在active_support/core_ext/enumerable.rb中定义。

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

注意:在active_support/core_ext/enumerable.rb中定义。

8.9 pick

方法pick从第一个元素中提取给定的键:

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

注意:在active_support/core_ext/enumerable.rb中定义。

9 Array的扩展

9.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_lastfirstlast是内置的)也是如此。感谢社会智慧和积极建设性,forty_two也可用。

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

注意:在active_support/core_ext/array/access.rb中定义。

9.2 提取

方法extract!移除并返回块返回true值的元素。如果没有给定块,则返回一个枚举器。

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]

注意:定义在active_support/core_ext/array/extract.rb中。

9.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中移除它。

注意:定义在active_support/core_ext/array/extract_options.rb中。

9.4 转换

9.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

注意:定义在active_support/core_ext/array/conversions.rb中。

9.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的相应调用。

注意:定义在active_support/core_ext/array/conversions.rb中。

9.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": ```ruby [Contributor.first, Commit.first].to_xml

=>

<?xml version="1.0" encoding="UTF-8"?>

4583

Aaron Batalion

53

aaron-batalion

Joshua Peek

2009-09-02T16:44:36Z

origin/master

2009-09-02T16:44:36Z

Joshua Peek

190316

false

Kill AMo observing wrap_with_notifications since ARes was only using it

723a47bfb3708f968821bc969a9a3fc873a3ed58


如果接收者是一个哈希数组,则默认的根元素也是“objects”:

```ruby
[{ 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>

注意:定义在active_support/core_ext/array/conversions.rb中。

9.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_ary,则不会调用to_a,而是返回一个以参数为其单个元素的数组。

最后一点特别值得比较一下对于一些可枚举对象:

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

还有一个相关的惯用法使用了展开运算符:

[*object]

注意:定义在active_support/core_ext/array/wrap.rb中。

9.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

注意:定义在active_support/core_ext/object/deep_dup.rb中。

9.7 分组

9.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不能用作填充值。

注意:定义在active_support/core_ext/array/grouping.rb中。

9.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不能用作填充值。

注意:定义在active_support/core_ext/array/grouping.rb中。

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

提示:观察前面的示例,连续的分隔符会导致空数组。

注意:定义在active_support/core_ext/array/grouping.rb中。

10 Hash的扩展

10.1 转换

10.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>

为此,该方法循环遍历键值对,并构建依赖于values的节点。给定一对keyvalue

  • 如果value是一个哈希表,则使用key作为:root进行递归调用。

  • 如果value是一个数组,则使用key作为:rootkey的单数形式作为:children进行递归调用。

  • 如果value是一个可调用对象,则它必须接受一个或两个参数。根据参数个数,使用options哈希作为第一个参数和key的单数形式作为第二个参数调用可调用对象。其返回值成为一个新节点。

  • 如果value响应to_xml方法,则使用key作为:root进行调用。

  • 否则,创建一个以key为标签的节点,其文本节点为value的字符串表示。如果valuenil,则添加一个设置为"true"的属性"nil"。除非存在且为true的选项:skip_types,否则还会根据以下映射添加一个名为"type"的属性: ruby 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等选项,它们会被转发给构建器。

注意:定义在active_support/core_ext/hash/conversions.rb中。

10.2 合并

Ruby有一个内置的方法Hash#merge,用于合并两个哈希:

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

Active Support定义了一些更方便的合并哈希的方法。

10.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!可能会更改调用者中的哈希,这可能是好事,也可能不是好事。

注意:定义在active_support/core_ext/hash/reverse_merge.rb中。

10.2.2 reverse_update

方法reverse_updatereverse_merge!的别名,如上所述。

警告:请注意,reverse_update没有感叹号。

注意:定义在active_support/core_ext/hash/reverse_merge.rb中。

10.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!会就地执行深度合并。

注意:定义在active_support/core_ext/hash/deep_merge.rb中。

10.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

注意:定义在active_support/core_ext/object/deep_dup.rb中。

10.4 处理键

10.4.1 exceptexcept!

方法except返回一个删除了参数列表中存在的键的哈希:

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

如果接收者响应convert_key,则在每个参数上调用该方法。这使得except可以与具有无关键访问的哈希很好地配合使用,例如:

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

还有一个带有感叹号的变体except!,它会就地删除键。

注意:定义在active_support/core_ext/hash/except.rb中。

10.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
# 结果将是
# => {"a"=>2}

这种方法可能很有用,例如可以轻松地接受符号和字符串作为选项。例如,ActionView::Helpers::FormHelper 定义了:

def to_check_box_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}}

注意:定义在 active_support/core_ext/hash/keys.rb 中。

10.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_text_area_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}}

注意:定义在 active_support/core_ext/hash/keys.rb 中。

10.4.4 to_optionsto_options!

to_optionsto_options! 方法是 symbolize_keyssymbolize_keys! 的别名。

注意:定义在 active_support/core_ext/hash/keys.rb 中。

10.4.5 assert_valid_keys

assert_valid_keys 方法接收任意数量的参数,并检查接收者是否有任何不在该列表中的键。如果有,则引发 ArgumentError

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

例如,在构建关联时,Active Record 不接受未知选项。它通过 assert_valid_keys 实现该控制。

注意:定义在 active_support/core_ext/hash/keys.rb 中。

10.5 处理值

10.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! 的变体,它通过使用块操作来破坏性地转换所有值。

注意:定义在 active_support/core_ext/hash/deep_transform_values.rb 中。

10.6 切片

slice! 方法用给定的键替换哈希,并返回一个包含已删除的键/值对的哈希。

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

注意:定义在 active_support/core_ext/hash/slice.rb 中。

10.7 提取

extract! 方法删除并返回与给定键匹配的键/值对。

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

extract! 方法返回与接收者相同的 Hash 子类。 ```ruby hash = { a: 1, b: 2 }.with_indifferent_access rest = hash.extract!(:a).class

=> ActiveSupport::HashWithIndifferentAccess


注意:定义在 `active_support/core_ext/hash/slice.rb` 中。


### 无差别访问

方法 [`with_indifferent_access`][Hash#with_indifferent_access] 返回一个 [`ActiveSupport::HashWithIndifferentAccess`][ActiveSupport::HashWithIndifferentAccess] 对象:

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

注意:定义在 active_support/core_ext/hash/indifferent_access.rb 中。

11 Regexp 的扩展

11.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

注意:定义在 active_support/core_ext/regexp.rb 中。

12 Range 的扩展

12.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 在条件中支持范围值时使用了这个子句。

注意:定义在 active_support/core_ext/range/conversions.rb 中。

12.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

注意:定义在 active_support/core_ext/range/compare_range.rb 中。

12.3 overlap?

方法 Range#overlap? 判断两个给定范围是否有非空交集:

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

注意:定义在 active_support/core_ext/range/overlap.rb 中。

13 Date 的扩展

13.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 文件以获取预期的行为。

13.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

注意:定义在 active_support/core_ext/date/calculations.rb 中。

13.1.2 命名日期

13.1.2.1 beginning_of_week, end_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

注意:定义在 active_support/core_ext/date_and_time/calculations.rb 中。

13.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

注意:定义在 active_support/core_ext/date_and_time/calculations.rb 中。

13.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 的行为如预期。

注意:定义在 active_support/core_ext/date_and_time/calculations.rb 中。

13.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

注意:定义在 active_support/core_ext/date_and_time/calculations.rb 中。

13.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

注意:定义在 active_support/core_ext/date_and_time/calculations.rb 中。

13.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

注意:定义在 active_support/core_ext/date_and_time/calculations.rb 中。

13.1.3 其他日期计算

13.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) 的简写。

注意:定义在 active_support/core_ext/date_and_time/calculations.rb 中。

13.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) 的简写。 注意:在active_support/core_ext/date_and_time/calculations.rb中定义。

13.1.3.3 weeks_ago

weeks_ago方法的工作方式与周类似:

Date.new(2010, 5, 24).weeks_ago(1)    # => Mon, 17 May 2010
Date.new(2010, 5, 24).weeks_ago(2)    # => Mon, 10 May 2010

注意:在active_support/core_ext/date_and_time/calculations.rb中定义。

13.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

请注意,在上面的示例中,增量可以是负数。

注意:在active_support/core_ext/date/calculations.rb中定义。

13.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

注意:在active_support/core_ext/date/calculations.rb中定义。

13.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

13.1.6 时间戳

INFO:如果可能,以下方法返回一个Time对象,否则返回一个DateTime对象。如果设置了,它们会遵守用户的时区。

13.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_day被别名为at_beginning_of_daymidnightat_midnight

注意:在active_support/core_ext/date/calculations.rb中定义。

13.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_hour被别名为at_beginning_of_hour

注意:在active_support/core_ext/date_time/calculations.rb中定义。

13.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_minute被别名为at_beginning_of_minute

INFO:beginning_of_hourend_of_hourbeginning_of_minuteend_of_minute适用于TimeDateTime,但不适用于Date,因为在Date实例上请求小时或分钟的开始或结束没有意义。

注意:在active_support/core_ext/date_time/calculations.rb中定义。

13.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

注意:在active_support/core_ext/date/calculations.rb中定义。

14 DateTime的扩展

警告:DateTime不了解夏令时规则,因此在夏令时变化时,其中一些方法可能存在边界情况。例如,seconds_since_midnight在这一天可能不会返回实际的秒数。

14.1 计算

DateTime类是Date类的子类,因此通过加载active_support/core_ext/date/calculations.rb,您继承了这些方法及其别名,只是它们始终返回日期时间。

以下方法已重新实现,因此您不需要为这些方法加载active_support/core_ext/date/calculations.rb

另一方面,advancechange也被定义并支持更多选项,下面将对其进行说明。

以下方法仅在active_support/core_ext/date_time/calculations.rb中实现,因为它们只在与DateTime实例一起使用时才有意义:

14.1.1 命名的日期时间

14.1.1.1 DateTime.current

Active Support定义DateTime.current类似于Time.now.to_datetime,只是它遵守用户的时区(如果定义)。实例谓词past?future?相对于DateTime.current进行定义。

注意:在active_support/core_ext/date_time/calculations.rb中定义。

14.1.2 其他扩展

14.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

注意:在active_support/core_ext/date_time/calculations.rb中定义。

14.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

注意:在active_support/core_ext/date_time/calculations.rb中定义。

14.1.2.3 utc?

谓词utc?表示接收器是否具有UTC作为其时区:

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

注意:在active_support/core_ext/date_time/calculations.rb中定义。

14.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不了解夏令时,您可能会在不存在的时间点上结束,而没有任何警告或错误告诉您。

注意:在active_support/core_ext/date_time/calculations.rb中定义。

14.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

注意:定义在active_support/core_ext/date_time/calculations.rb中。

14.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

15 Time的扩展

15.1 计算

它们是类似的。请参考上面的文档,并考虑以下差异:

  • change接受一个额外的:usec选项。
  • Time理解夏令时,因此您会得到正确的夏令时计算,如下所示:
Time.zone_default
# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>

# 在巴塞罗那,2010/03/28 02:00 +0100 由于夏令时变为 2010/03/28 03:00 +0200。
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对象。

15.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

注意:定义在active_support/core_ext/time/calculations.rb中。

15.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

注意:定义在active_support/core_ext/date_and_time/calculations.rb中。

15.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

注意:定义在active_support/core_ext/time/calculations.rb中。

15.1.4 prev_monthnext_month

prev_monthnext_month返回上个月或下个月的相同日期的时间: ruby 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

注意:定义在 active_support/core_ext/time/calculations.rb 中。

15.1.5 prev_year, next_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日,则返回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

注意:定义在 active_support/core_ext/time/calculations.rb 中。

15.1.6 prev_quarter, next_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_quarter 的别名是 last_quarter

注意:定义在 active_support/core_ext/date_and_time/calculations.rb 中。

15.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 支持的范围,微秒将被丢弃,并返回一个 DateTime 对象。

15.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

16 File 的扩展

16.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进行追加。

辅助文件将写入一个标准的临时文件目录,但您可以将自定义目录作为第二个参数传递。

注意:在active_support/core_ext/file/atomic.rb中定义。

17 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

注意:在active_support/core_ext/name_error.rb中定义。

18 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

注意:在active_support/core_ext/load_error.rb中定义。

19 Pathname的扩展

19.1 existence

existence方法如果指定的文件存在,则返回接收器,否则返回nil。这对于以下习惯用法很有用:

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

注意:在active_support/core_ext/pathname/existence.rb中定义。

反馈

欢迎您帮助改进本指南的质量。

如果您发现任何拼写错误或事实错误,请贡献您的意见。 要开始,请阅读我们的 文档贡献 部分。

您还可能会发现不完整的内容或过时的内容。 请为主要内容添加任何缺失的文档。请先检查 Edge 指南,以验证问题是否已经修复或尚未修复。 请参阅 Ruby on Rails 指南准则 以了解样式和规范。

如果您发现需要修复但无法自行修复的问题,请 提交问题

最后但同样重要的是,欢迎您在 官方 Ruby on Rails 论坛 上讨论有关 Ruby on Rails 文档的任何问题。