DateTimeOffset
представляет собой мгновенное время (также известное как абсолютное время ). Под этим я подразумеваю момент времени, универсальный для всех (не считая високосных секунд или релятивистских эффектов замедления времени ). Другой способ представить мгновенное время - это DateTime
, где .Kind
равно DateTimeKind.Utc
.
Это отличается от календарного времени (также известного как гражданское время ), которое является позицией в чьем-либо календаре, и по всему миру существует множество различных календарей. Мы называем эти календари часовые пояса . Календарное время представлено DateTime
, где .Kind
равно DateTimeKind.Unspecified
или DateTimeKind.Local
. И .Local
имеет смысл только в тех случаях, когда у вас есть подразумеваемое понимание того, где расположен компьютер, который использует результат. (Например, рабочая станция пользователя)
Итак, почему DateTimeOffset
вместо UTC DateTime
? Все дело в перспективе. Давайте использовать аналогию - мы будем притворяться фотографами.
Представьте, что вы стоите на временной шкале календаря, направляя камеру на человека на мгновенной временной шкале, расположенной перед вами. Вы устанавливаете камеру в соответствии с правилами вашего часового пояса, которые периодически меняются из-за перехода на летнее время или из-за других изменений в юридическом определении вашего часового пояса. (У вас нет устойчивой руки, поэтому ваша камера дрожит.)
Человек, стоящий на фотографии, увидит угол, под которым взята ваша камера. Если бы другие фотографировали, они могли быть с разных сторон. Это то, что представляет Offset
часть DateTimeOffset
.
Так, если вы маркируете свою камеру «Восточное время», иногда вы указываете от -5, а иногда - от -4. Во всем мире есть камеры, все они помечены разными вещами, и все они указывают на одну и ту же мгновенную шкалу времени под разными углами. Некоторые из них расположены рядом друг с другом (или друг над другом), поэтому просто знать, что смещение недостаточно, чтобы определить, к какому часовому поясу относится время.
А как насчет UTC? Ну, это единственная камера, у которой гарантированно устойчивая рука. Это на штативе, прочно закрепленном на земле. Это никуда не денется. Мы называем угол его перспективы нулевым смещением.
Итак - что говорит нам эта аналогия? Это обеспечивает некоторые интуитивные руководящие принципы.
Если вы представляете время относительно какого-то конкретного места, представьте его в календарном времени с помощью DateTime
. Просто убедитесь, что вы никогда не перепутаете один календарь с другим. Unspecified
должно быть вашим предположением. Local
полезно только с DateTime.Now
. Например, я могу получить DateTime.Now
и сохранить его в базе данных - но когда я получаю его, я должен предположить, что это Unspecified
. Я не могу полагать, что мой локальный календарь - это тот же календарь, из которого он был изначально взят.
Если вы всегда должны быть уверены в моменте, убедитесь, что вы представляете мгновенное время. Используйте DateTimeOffset
для обеспечения его соблюдения или используйте UTC DateTime
по соглашению.
Если вам нужно отследить момент мгновенного времени, но вы также хотите знать: «Сколько времени пользователь думал, что это было в его локальном календаре?» - тогда вы должны использовать DateTimeOffset
. Это очень важно, например, для систем хронометража - как для технических, так и для юридических вопросов.
Если вам когда-либо понадобится изменить ранее записанный DateTimeOffset
- у вас недостаточно информации только для одного смещения, чтобы гарантировать, что новое смещение все еще актуально для пользователя. Вы должны также сохранить идентификатор часового пояса (подумайте - мне нужно имя этой камеры, чтобы я мог сделать новый снимок, даже если положение изменилось).
Следует также отметить, что Noda Time имеет для этого представление, называемое ZonedDateTime
, в то время как библиотека базовых классов .Net не имеет ничего подобного. Вам нужно будет хранить значения DateTimeOffset
и TimeZoneInfo.Id
.
Иногда вам может потребоваться указать календарное время, которое является локальным для «того, кто смотрит на него». Например, при определении значения сегодня . Сегодня всегда полночь - полночь, но они представляют собой почти бесконечное количество перекрывающихся диапазонов на мгновенной временной шкале. (На практике у нас есть ограниченное количество часовых поясов, но вы можете выразить смещения вплоть до отметки). Поэтому в этих ситуациях убедитесь, что вы понимаете, как ограничить «кто спрашивает?» вопрос до одного часового пояса или, если нужно, перевод их обратно в мгновенное время.
Вот еще несколько небольших подробностей о DateTimeOffset
, которые подтверждают эту аналогию, и несколько советов, как сохранить ее прямо:
Если вы сравните два значения DateTimeOffset
, они сначала нормализуются к нулевому смещению перед сравнением. Другими словами, 2012-01-01T00:00:00+00:00
и 2012-01-01T02:00:00+02:00
относятся к одному и тому же мгновенному моменту и поэтому эквивалентны.
Если вы проводите какое-либо модульное тестирование и хотите убедиться в смещении, протестируйте и значение DateTimeOffset
, и свойство .Offset
отдельно.
В платформу .Net встроено одностороннее неявное преобразование, которое позволяет передавать DateTime
в любой параметр или переменную DateTimeOffset
. При этом .Kind
имеет значение . Если вы передаете тип UTC, он будет иметь нулевое смещение, но если вы передадите либо .Local
, либо .Unspecified
, он будет считаться local . Фреймворк в основном говорит: «Ну, вы попросили меня перевести календарное время в мгновенное, но я понятия не имею, откуда это пришло, поэтому я просто собираюсь использовать местный календарь». Это огромная ошибка, если вы загрузите неуказанный DateTime
на компьютер с другим часовым поясом. (ИМХО - это должно вызвать исключение - но это не так.)
Бесстыдная вилка:
Многие люди поделились со мной, что они находят эту аналогию чрезвычайно ценной, поэтому я включил ее в свой курс Pluralsight, Основы даты и времени . Пошаговое руководство по аналогии с камерой вы найдете во втором модуле «Вопросы контекста» в клипе под названием «Время календаря и мгновенное время».