这是一个非常好的问题。简短的回答是 1.month 是一个 ActiveSupport::Duration 对象(正如您已经看到的),它的身份以两种不同的方式定义:
- 如
30.days(如果您需要/尝试将其转换为秒数),和
- 1 个月(如果您尝试将此持续时间添加到日期)。
通过查看它的parts方法,你可以看到它仍然知道它相当于1个月:
main > 1.month.parts
=> [[:months, 1]]
一旦你看到它仍然知道正好是 1 个月的证据,那么像 Time.utc(2012,2,1) + 1.month 这样的计算如何即使对于没有正好 29 天的月份也能给出正确的结果,以及为什么它给出不同的结果,就不再那么神秘了比Time.utc(2012,2,1) + 30.days 给出的要多。
ActiveSupport::Duration如何隐藏真实身份?
对我来说真正的谜团在于它如何隐藏其真实身份。我们知道它是一个ActiveSupport::Duration 对象,但很难承认它是一个对象!
当您在控制台中检查它时(我使用的是 Pry),它看起来与(并声称 是)一个普通的 Fixnum 对象一模一样:
main > one_month = 1.month
=> 2592000
main > one_month.class
=> Fixnum
它甚至声称等同于30.days(或2592000.seconds),我们已经证明这是不正确的(至少不是在所有情况下):
main > one_month = 1.month
=> 2592000
main > thirty_days = 30.days
=> 2592000
main > one_month == thirty_days
=> true
main > one_month == 2592000
=> true
所以要判断一个对象是否为ActiveSupport::Duration,不能依赖class 方法。相反,你必须直截了当地问它:“你是不是 ActiveSupport::Duration 的实例?”面对如此直截了当的问题,被质疑的对象将不得不坦白:
main > one_month.is_a? ActiveSupport::Duration
=> true
另一方面,单纯的 Fixnum 对象必须低头并承认它们不是:
main > 2592000.is_a? ActiveSupport::Duration
=> false
您还可以通过检查它是否响应 :parts 来将其与常规 Fixnums 区分开来:
main > one_month.parts
=> [[:months, 1]]
main > 2592000.parts
NoMethodError: undefined method `parts' for 2592000:Fixnum
from (pry):60:in `__pry__'
拥有一系列部件很棒
拥有一组部件的好处在于它允许您将持续时间定义为混合单位,如下所示:
main > (one_month + 5.days).parts
=> [[:months, 1], [:days, 5]]
这使它能够准确地计算如下内容:
main > Time.utc(2012,2,1) + (one_month + 5.days)
=> 2012-03-06 00:00:00 UTC
...如果它只是仅存储了天或秒,它将不能够正确计算 作为它的值。如果我们首先将 1.month 转换为其“等效”秒数或天数,您可以自己看到这一点:
main > Time.utc(2012,2,1) + (one_month + 5.days).to_i
=> 2012-03-07 00:00:00 UTC
main > Time.utc(2012,2,1) + (30.days + 5.days)
=> 2012-03-07 00:00:00 UTC
ActiveSupport::Duration 是如何工作的? (血腥的实现细节)
ActiveSupport::Duration 实际上被定义为(在gems/activesupport-3.2.13/lib/active_support/duration.rb 中)作为BasicObject 的子类,根据docs,“可用于创建独立于Ruby 的对象层次结构的对象层次结构,代理对象如Delegator类,或其他必须避免来自 Ruby 方法和类的命名空间污染的用途。”
ActiveSupport::Duration 使用method_missing 将方法委托给它的@value 变量。
额外问题:有谁知道为什么ActiveSupport::Duration 对象声称不响应:parts,即使它实际上确实,为什么parts方法没有在方法列表中列出?
main > 1.month.respond_to? :parts
=> false
main > 1.month.methods.include? :parts
=> false
main > 1.month.methods.include? :since
=> true
答案:因为BasicObject 没有定义respond_to? 方法,所以发送respond_to? 到ActiveSupport::Duration 对象最终会调用它的method_missing 方法,如下所示:
def method_missing(method, *args, &block) #:nodoc:
value.send(method, *args, &block)
end
1.month.value 只是 Fixnum 2592000,所以它实际上最终调用了2592000.respond_to? :parts,当然是false。
不过,这很容易解决,只需将 respond_to? 方法添加到 ActiveSupport::Duration 类:
main > ActiveSupport::Duration.class_eval do
def respond_to?(name, include_private = false)
[:value, :parts].include?(name) or
value.respond_to?(name, include_private) or
super
end
end
=> nil
main > 1.month.respond_to? :parts
=> true
为什么methods 错误地省略了:parts 方法的解释是相同的:因为methods 消息只是被委托给值,当然没有 有parts方法。我们可以像添加我们自己的 methods 方法一样轻松地修复这个错误:
main > ActiveSupport::Duration.class_eval do
def methods(*args)
[:value, :parts] | super
end
end
=> nil
main > 1.month.methods.include? :parts
=> true