Это действительно хороший вопрос.Короткий ответ: 1.month
является ActiveSupport::Duration
объектом (как вы уже видели), и его идентичность определяется двумя различными способами:
- как
30.days
(на случай, если вам нужно / попробоватьчтобы преобразовать его в число секунд), * 1007 * и - как 1 месяц (в случае, если вы попытаетесь добавить эту продолжительность к дате).
Youможно увидеть, что он все еще знает, что он эквивалентен 1 месяцу, проверив его метод parts
:
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
Вы также можете отличить его от обычных Fixnum, проверив, отвечает ли он на :parts
:
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
?(Подробности реализации Gory)
ActiveSupport::Duration
фактически определено (в gems/activesupport-3.2.13/lib/active_support/duration.rb
) как подкласс BasicObject
, который в соответствии с документами , "может использоватьсядля создания иерархий объектов, независимых от иерархии объектов Ruby, используются прокси-объекты, такие как класс Delegator, или другие области применения, в которых следует избегать загрязнения пространства имен методами и классами Ruby. "
ActiveSupport::Duration
использует method_missing
для делегирования методовего @value
переменная.
Бонусный вопрос : Кто-нибудь знает, почему ActiveSupport::Duration
объект утверждает, что не отвечает на :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