НейроАгент

DateTime vs DateTimeOffset: Полное руководство

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

В чем разница между DateTime и DateTimeOffset в .NET, и когда следует использовать каждый из них?

В настоящее время наша команда обрабатывает объекты .NET DateTime с учетом часового пояса следующим образом:

  • Создание объектов DateTime в формате UTC (с использованием DateTime.UtcNow)
  • Преобразование из UTC в локальное время пользователя для отображения

Я недавно узнал о типе DateTimeOffset, который, похоже, захватывает как локальное, так и UTC время в одном объекте. Каковы ключевые различия между этими двумя типами даты/времени, и какие существуют лучшие практики для выбора между ними в различных сценариях?

НейроАгент

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


Содержание


Основные различия между DateTime и DateTimeOffset

Фундаментальное различие между DateTime и DateTimeOffset заключается в том, как они представляют информацию о часовом поясе и их способности однозначно идентифицировать конкретные моменты времени.

Структура DateTime

DateTime хранит значение даты и времени вместе со свойством Kind, которое может быть:

  • DateTimeKind.Utc - представляет время UTC
  • DateTimeKind.Local - представляет локальное время системы
  • DateTimeKind.Unspecified - указывает на отсутствие конкретного часового пояса

Этот подход создает неоднозначность, потому что DateTimeKind.Local и DateTimeKind.Utc на самом деле не хранят информацию о смещении, которая была бы необходима для надежного преобразования между часовыми поясами.

Структура DateTimeOffset

DateTimeOffset, как объясняется в Microsoft Learn, “включает значение даты и времени вместе со смещением, которое показывает, насколько это время отличается от UTC”. Это означает, что он всегда знает точно, насколько он удален от Всемирного координированного времени (UTC), что делает его однозначным в отношении конкретного момента времени, который он представляет.

csharp
// DateTime со свойством Kind
var dateTime = DateTime.UtcNow; // Kind = Utc

// DateTimeOffset с явным смещением
var dateTimeOffset = new DateTimeOffset(DateTime.UtcNow, TimeSpan.Zero); // Offset = 00:00

Ключевое преимущество DateTimeOffset заключается в том, что, согласно документации Microsoft, “он отражает смещение времени от UTC, но не отражает фактический часовой пояс, которому принадлежит это смещение”. Это различие важно - хотя он знает смещение, он не хранит название часового пояса или историческую информацию о летнем времени.


Понимание свойства DateTimeKind

Свойство DateTime.Kind является критически важным аспектом DateTime, который часто приводит к путанице и ошибкам в приложениях.

Значения DateTimeKind и их последствия

  • DateTimeKind.Utc: Время хранится как время UTC. При преобразовании в локальное время .NET применяет текущее смещение часового пояса системы.
  • DateTimeKind.Local: Время хранится как локальное время системы. При преобразовании в UTC .NET применяет текущее смещение часового пояса системы.
  • DateTimeKind.Unspecified: Часовой пояс неизвестен. Это наиболее проблематично, поскольку методы преобразования обрабатывают его по-разному в зависимости от контекста.
csharp
// Поведение общих преобразований
var unspecified = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Unspecified);

// ToUniversalTime() предполагает, что время локальное и преобразует соответственно
var utcFromUnspecified = unspecified.ToUniversalTime(); // Может добавить/вычесть смещение часового пояса

// При сравнении Unspecified обрабатывается как UTC

Это поведение создает “трудно обнаруживаемые ошибки”, как отмечено в нескольких обсуждениях, где значения DateTimeKind.Unspecified обрабатываются непоследовательно в разных API и методах преобразования.

Альтернатива DateTimeOffset

DateTimeOffset устраняет большую часть этой неоднозначности, всегда зная свое смещение относительно UTC:

csharp
// DateTimeOffset всегда знает свое смещение
var dto = new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.FromHours(-5));
// Это однозначно представляет 12:00 EST 1 января 2024 года

Как отмечает один из разработчиков, “DateTimeOffset содержит больше информации и гораздо лучше обрабатывает различия в часовых поясах”, потому что он “не может действительно ошибиться с неправильными вызовами DateTime.Now вместо DateTime.UtcNow.”


Когда использовать DateTime против DateTimeOffset

Выбор между DateTime и DateTimeOffset зависит от вашего конкретного случая использования и требований к точности и обработке часовых поясов.

Рекомендуемые сценарии для DateTime

Простые операции с датой и временем
Когда вы работаете с датами и временем, где информация о часовом поясе не имеет значения:

csharp
// Дни рождения, даты истечения срока, время приема (когда часовой пояс не важен)
var birthday = new DateTime(1990, 5, 15);
var expiration = DateTime.Now.AddDays(30);

Критически важные для производительности приложения
Согласно тестам производительности, операции с DateTime обычно быстрее, так как им не нужно поддерживать информацию о смещении.

Интеграция со старыми системами
При работе с существующими системами, которые ожидают значения DateTime или имеют установленные шаблоны вокруг DateTimeKind.

Рекомендуемые сценарии для DateTimeOffset

Ведение журнала аудита и отслеживание событий
Когда вам нужно записать точное время, когда что-то произошло:

csharp
// Регистрация пользователя, системные события, финансовые транзакции
var registrationTime = DateTimeOffset.UtcNow;
var eventTime = DateTimeOffset.Now;

Межсистемное взаимодействие
Как отмечено в Microsoft Learn, “DateTimeOffset является лучшим типом данных для сохранения данных приложения в базе данных, поскольку он однозначно идентифицирует одну точку во времени.”

Вычисления с учетом часового пояса
Когда вам нужно выполнять вычисления в разных часовых поясах:

csharp
// Планирование встреч в разных часовых поясах
var meetingTime = new DateTimeOffset(2024, 6, 15, 14, 0, 0, TimeSpan.FromHours(-7)); // 2 PM PST
var meetingTimeUtc = meetingTime.UtcDateTime; // Автоматически преобразуется в UTC

Хранение в базе данных
Согласно мнению профессиональных разработчиков, “DateTimeOffset связывает текущее значение с часовым поясом, сохраняя смещение относительно даты и времени UTC”, что устраняет многие ошибки, связанные с часовыми поясами, которые возникают при использовании DateTime.

Блок-схема принятия решений

Вам нужно представить конкретный момент времени?
├── ДА → Нужно ли, чтобы он был однозначным на разных системах/в разных часовых поясах?
│       ├── ДА → Используйте DateTimeOffset
│       └── НЕТ → Используйте DateTime с DateTimeKind.Utc
└── НЕТ → Используйте DateTime (часовой пояс не важен)

Методы преобразования и лучшие практики

Правильное преобразование между DateTime и DateTimeOffset критически важно для поддержания целостности данных и предотвращения ошибок, связанных с часовыми поясами.

Ключевые свойства преобразования

DateTimeOffset в DateTime

Свойство UtcDateTime является наиболее надежным способом преобразования DateTimeOffset в DateTime:

csharp
var dto = new DateTimeOffset(2024, 1, 1, 12, 0, 0, TimeSpan.FromHours(-5));
var utcDateTime = dto.UtcDateTime; // Kind = Utc, значение скорректировано под UTC

Свойство DateTime возвращает компонент даты и времени без смещения:

csharp
var dateTime = dto.DateTime; // Kind = Unspecified, нет информации о часовом поясе

DateTime в DateTimeOffset

Преобразование DateTime в DateTimeOffset straightforward:

csharp
var utcDateTime = DateTime.UtcNow;
var dto = new DateTimeOffset(utcDateTime); // Offset = TimeSpan.Zero

var localDateTime = DateTime.Now;
var localDto = new DateTimeOffset(localDateTime); // Offset = смещение локального часового пояса

Лучшие практики обработки часовых поясов

Храните в UTC, отображайте в локальном времени
Ваш текущий подход является широко принятым шаблоном:

csharp
// Хранение
var createdTime = DateTime.UtcNow; // Хранить как UTC

// Отображение
var displayTime = createdTime.ToLocalTime(); // Преобразовать в локальное время пользователя

Используйте DateTimeOffset для сложных операций
При работе с несколькими часовыми поясами:

csharp
// Время встреч в разных часовых поясах
var meetingTime = new DateTimeOffset(2024, 6, 15, 14, 0, 0, TimeSpan.FromHours(-7)); // 2 PM PST
var easternTime = meetingTime.ToOffset(TimeSpan.FromHours(-4)); // 5 PM EST
var utcTime = meetingTime.UtcDateTime; // 9 PM UTC

Внимательно обрабатывайте сериализацию
Как отмечено в блоге Дэвида Рикарда, “Используйте DateTimeOffset и будьте осторожны с сериализацией”. Не все форматы сериализации правильно обрабатывают DateTimeOffset.

Используйте инвариантные относительно культуры форматы
Для сериализации “только форматы ‘o’, ‘r’, ‘s’ и ‘u’ являются инвариантными относительно культуры”, как упоминается в том же источнике.


Распространенные проблемы и решения

Работа с типами даты и времени в .NET может привести к нескольким распространенным проблемам, вызывающим ошибки и несогласованность данных.

Проблемы с DateTimeKind.Unspecified

Тип DateTimeKind.Unspecified особенно проблематичен, поскольку его поведение зависит от контекста:

csharp
var unspecified = new DateTime(2024, 1, 1, 12, 0, 0, DateTimeKind.Unspecified);

// Различное поведение преобразования
var toUtc = unspecified.ToUniversalTime(); // Обрабатывается как локальное время
var toLocal = unspecified.ToLocalTime();   // Обрабатывается как локальное время

// В SQL Server это может обрабатываться иначе
// При сериализации в JSON поведение варьируется в зависимости от библиотеки

Решение: Всегда явно указывайте DateTimeKind или используйте DateTimeOffset для избежания неоднозначности.

Проблемы с переходом на летнее время

DateTime может давать неверные результаты во время переходов на летнее время:

csharp
// Во время перехода на летнее время (например, 5 ноября 2023 года в 2 часа ночи)
var ambiguousTime = new DateTime(2023, 11, 5, 1, 30, 0, DateTimeKind.Local);
// Это время может быть интерпретировано либо до, либо после перехода

Решение: Используйте DateTimeOffset, который более корректно обрабатывает переходы на летнее время:

csharp
var unambiguousTime = new DateTimeOffset(2023, 11, 5, 1, 30, 0, TimeSpan.FromHours(-7));
// Смещение делает точный момент времени ясным

Проблемы хранения в базе данных

При хранении DateTime в базах данных без правильной обработки часового пояса:

csharp
// Проблема: потеря информации о часовом поясе
var localTime = DateTime.Now;
// При хранении в столбцах "TIMESTAMP WITHOUT TIME ZONE" информация о часовом поясе теряется

Решение: Используйте DateTimeOffset или явно храните UTC DateTime:

csharp
// Лучше: хранить с информацией о часовом поясе
var offsetTime = DateTimeOffset.Now;
// Или явно хранить UTC
var utcTime = DateTime.UtcNow;

Ошибки преобразования часовых поясов

Распространенные ошибки преобразования, приводящие к неверному времени:

csharp
// Проблема: двойное преобразование
var localTime = DateTime.Now;
var utcTime = localTime.ToUniversalTime();
var backToLocal = utcTime.ToLocalTime(); // Исходное локальное время, а не текущее локальное время

Решение: Используйте DateTimeOffset, который обрабатывает преобразования более предсказуемо:

csharp
// Лучше: однократное преобразование
var dto = new DateTimeOffset(DateTime.Now);
var convertedDto = dto.ToOffset(TimeSpan.FromHours(-5)); // Прямая корректировка смещения

Как отмечают разработчики на Reddit, это как раз те виды проблем, которые “вы не можете действительно ошибиться” с использованием DateTimeOffset.


Вопросы производительности

Хотя и DateTime, и DateTimeOffset являются легковесными типами, есть различия в производительности, которые следует учитывать в высокопроизводительных сценариях.

Использование памяти

  • DateTime: 8 байт (64-бит)
  • DateTimeOffset: 16 байт (64-бит DateTime + 64-бит смещение)

Это означает, что DateTimeOffset использует в два раза больше памяти, что может быть значительным в крупных приложениях.

Производительность операций

Согласно различным тестам производительности и обсуждениям в сообществе:

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

Когда производительность важна

Используйте DateTime для:

  • Крупных коллекций значений даты и времени
  • Критически важных для производительности вычислений
  • Систем, где использование памяти является проблемой

Используйте DateTimeOffset для:

  • Журналов аудита и ведения журналов
  • Пользовательских интерфейсов с учетом часового пояса
  • Систем, где надежность важнее производительности

Гибридный подход

Многие успешные приложения используют гибридный подход:

csharp
// Используйте DateTime для внутреннего хранения и вычислений
private DateTime _internalUtcTime;

// Преобразуйте в DateTimeOffset только при необходимости для отображения или ведения журнала
public DateTimeOffset DisplayTime => new DateTimeOffset(_internalUtcTime);

Этот подход сочетает преимущества производительности с безопасностью DateTimeOffset при необходимости для конкретных операций.


Заключение

Ключевые выводы

  1. DateTimeOffset обеспечивает однозначное представление времени, включая смещение от UTC, в то время как свойство Kind DateTime создает неоднозначность относительно фактического часового пояса.
  2. DateTime остается подходящим для простых операций с датой и временем, когда информация о часовом поясе не критична, предлагая лучшую производительность и меньший размер в памяти.
  3. Ваш текущий подход хранения в UTC является допустимым, но DateTimeOffset обеспечивает дополнительную безопасность для операций с учетом часового пояса без ошибок преобразования.
  4. Преобразование между типами требует тщательного рассмотрения - используйте свойство UtcDateTime для надежного преобразования DateTimeOffset в DateTime.
  5. DateTimeOffset отлично подходит для аудита и межсистемных сценариев, где точное время и обработка часовых поясов являются критически важными.

Практические рекомендации

  • Для новых приложений: Рассмотрите возможность использования DateTimeOffset по умолчанию для всех операций с датой и временем, ориентированных на пользователя
  • Для существующих систем на основе DateTime: Постепенно переходите на DateTimeOffset там, где неоднозначность часового пояса вызывает проблемы
  • Для хранения в базе данных: Используйте DateTimeOffset или явный UTC DateTime с правильными типами столбцов
  • Для критически важного для производительности кода: Используйте DateTime внутренне, но преобразуйте в DateTimeOffset для отображения/ведения журнала
  • Для API: Используйте DateTimeOffset в объектах запросов/ответов для обеспечения согласованности представления времени

Когда сомневаетесь

Как отмечают несколько опытных разработчиков, “в 99% случаев можно придерживаться UTC везде”, но для оставшегося 1%, где важна сложность часовых поясов, DateTimeOffset обеспечивает надежное решение. Учитывайте ваши конкретные требования и выбирайте подход, который лучше всего сбалансирует производительность, поддерживаемость и правильность для нужд вашего приложения.


Источники

  1. Compare types related to date and time - .NET | Microsoft Learn
  2. Converting between DateTime and DateTimeOffset - .NET | Microsoft Learn
  3. DateTimeOffset vs DateTime in C# - Code Maze
  4. DateTime and DateTimeOffset in .NET: Good practices and common pitfalls – David Rickard’s Tech Blog
  5. DateTime vs DateTimeOffset - Stack Overflow
  6. DateTimeOffset vs DateTime for CreatedBy and ModifiedBy properties on entities - r/dotnet
  7. DateTime Best Practices In .NET C# - Development Simply Put
  8. Why Use DateTimeOffset | Ardalis Blog
  9. DateTimeOffset.LocalDateTime Property - Microsoft Learn
  10. DateTimeOffset.UtcDateTime Property - Microsoft Learn