Недавно мы столкнулись с рядом ошибок, связанных с DateTime не в формате UTC. Типичный способ получить один из них - вызвать DateTime.Now, , что каждый разработчик C #, вероятно, делал сотни раз.

Но, как это часто бывает, вещи, которые так легко набирать, на самом деле скрывают гнездо гадюк.

Типы ошибок, которые мы наблюдаем, относятся к пользователям в часовых поясах со смещением по всемирному координированному времени. Для нас это особенно коварно, потому что мы живем и работаем в UTC + 0.

DateTime считается вредным

Как разумно, мы стараемся использовать UTC везде, где мы можем внутри, и конвертируем только в местное время и обратно по краям (ввод или отображение времени). Но одно-единственное случайное использование DateTime, отличного от UTC, как может случиться при неосторожном вызове DateTime.Now, открывает дверь в мир загадочных ошибок.

Так что же происходит? Ну, DateTime не может правильно представлять момент времени.

Здесь, в Великобритании, 1216 часов. В Москве 15.16. Но это не значит, что Москва находится на три часа вперед - мы живем в один и тот же момент во времени. Они просто устанавливают там свои часы по-другому, чтобы полдень приходился на разумное время - в Москве по-прежнему 1216 UTC.

Почему это проблема для DateTime? DateTime имеет свойство Kind, которое может быть Utc или Local (или Unspecified, но пока оставим это). Но даже если он является местным, на самом деле он не содержит никакой информации о том, из какого часового пояса он пришел. Преобразование DateTime из Local в Utc (или наоборот) использует настройку часового пояса устройства.

Так что, возможно, если бы все время было UTC, все было бы хорошо. Почему бы нам просто не научиться использовать DateTime.UtcNow везде? Что ж, вы должны помнить об этом каждый раз, иначе будет хаос. DateTime не помешает вам выполнять арифметические операции с одним Local и одним временем в формате UTC, и результаты вряд ли будут такими, как вы ожидаете.

Здесь у нас есть два (локальных) DateTimes. Мы можем подумать, что они представляют один и тот же момент (UTC), но это не так. Вычитание двух значений времени дает нам разницу в смещениях двух часовых поясов, что, вероятно, не то, что мы хотели. И, что очень важно, изменение строки 5 на DateTime.UtcNow не помогает.

В этом примере показано, что DateTime.UtcNow минус DateTime.Now будет отличным от нуля, если часовой пояс имеет смещение относительно UTC. Что важно для сортировки, одно из них будет считаться произошедшим раньше другого!

Ошибки DateTime хорошо известны (вот документ MS 2004 года), но он сохраняется во многих широко используемых API. И это может быть действительно полезно в некоторых сценариях - если вы действительно уверены, что вас не волнуют UTC, летнее время и друзья.

Введите DateTimeOffset

DateTimeOffset содержит DateTime и смещение исходного UTC - ключевую часть информации, которая отсутствует в DateTime. Давайте изменим наш код, чтобы теперь использовать DateTimeOffset.

Легко - и мы получаем ожидаемую разницу, потому что здесь может быть не то же время, что и в Нью-Йорке, это тот же момент. Это может устранить целый класс ошибок - и вам не нужно забывать вводить UtcNow.

Многие API-интерфейсы фреймворка, которые раньше принимали параметры DateTime, теперь получают перегрузки, принимающие DateTimeOffsets. Для взаимодействия со старыми API-интерфейсами DateTimeOffset предоставляет свойства .DateTime и .UtcDateTime, которые возвращают вам обычный DateTime (правильного типа).

Лично мне нравятся такие решения, где просто вопрос использования правильного типа для получения правильных гарантий. Сложнее всего будет изменить нашу мышечную память ...