Введение: Почему у вас болит голова от дат?
Если вы когда-нибудь писали код на C# и сталкивались с задачей передать дату и время между сервером в Москве и клиентом в Нью-Йорке, вы наверняка замечали странные баги: «А почему у меня завтра?», «Откуда взялось 3 часа?». Знакомая ситуация?
В .NET есть два главных инструмента для работы с датами: DateTime и DateTimeOffset. На первый взгляд они похожи — оба хранят дату и время. Но разница между ними ключевая, и неправильный выбор может привести к катастрофическим ошибкам в бизнес-логике.
В этом туториале мы разберем DateTime vs DateTimeOffset на реальных примерах, поймем, когда что использовать, и навсегда забудем о путанице с часовыми поясами. Поехали!
Что такое DateTime? (И почему это ловушка)
DateTime — это структура, которая хранит дату и время. Она существует в .NET с самой первой версии. Но у неё есть одна особенность: она не хранит информацию о часовом поясе. Точнее, хранит, но только в виде флага Kind:
DateTimeKind.Unspecified— пояс не указан (по умолчанию).DateTimeKind.Utc— время UTC.DateTimeKind.Local— местное время системы.
Проблема в том, что этот флаг — просто информация. Он не конвертирует время автоматически. Представьте, что вы сохраняете в базу данных DateTime с Kind = Local на сервере в Москве. Потом вы читаете эту дату на сервере во Владивостоке. Что произойдет? Правильно — программа увидит, что Kind = Local, и подумает, что это местное время Владивостока. А на самом деле это Москва. Абсолютная катастрофа.
// Пример проблемы с DateTime
DateTime moscowTime = new DateTime(2025, 3, 15, 14, 0, 0, DateTimeKind.Local);
Console.WriteLine(moscowTime); // 15.03.2025 14:00:00 (предполагается, что это MSK)
// Если этот код запустить на сервере с часовым поясом UTC+10 (Владивосток)
// DateTime будет интерпретировано как местное время Владивостока!
// Что приведет к ошибке в 7 часов.
Что такое DateTimeOffset? (Спаситель разработчиков)
DateTimeOffset — это более умная структура, которая появилась в .NET Framework 3.5. Она хранит дату, время и смещение относительно UTC (Offset). Смещение — это число часов и минут, на которое ваше локальное время отличается от UTC.
Например: 2025-03-15 14:00:00 +03:00 — это время в Москве (UTC+3). Если вы передадите это значение на сервер во Владивостоке (UTC+10), .NET поймет: «Ага, это +03, значит UTC = 11:00. Теперь я могу легко пересчитать в местное время: 21:00». Никакой магии — чистая математика.
// Пример с DateTimeOffset
DateTimeOffset moscowOffset = new DateTimeOffset(2025, 3, 15, 14, 0, 0, TimeSpan.FromHours(3));
Console.WriteLine(moscowOffset); // 15.03.2025 14:00:00 +03:00
// Теперь, даже на сервере во Владивостоке, мы знаем точное смещение
// и можем корректно отобразить время для пользователя
Ключевые различия: DateTime vs DateTimeOffset
Давайте сведем всё в наглядную таблицу. Это поможет вам быстро понять, что выбрать в конкретной ситуации.
1. Хранение информации о часовом поясе
- DateTime: Хранит только флаг Kind (Unspecified, Utc, Local). Не хранит само смещение.
- DateTimeOffset: Хранит точное смещение от UTC (например, +03:00, -05:00).
2. Сравнение значений
- DateTime: Сравнивает по «голому» значению. Если у вас 10:00 UTC и 10:00 MSK (UTC+3), то
DateTime.Equals()скажет, что они равны. Хотя на самом деле это разные моменты времени. - DateTimeOffset: Сравнивает по моменту времени (UTC). 10:00 UTC и 13:00 MSK будут считаться равными, так как это один и тот же момент.
// Сравнение DateTime vs DateTimeOffset
DateTime utcTime = new DateTime(2025, 3, 15, 10, 0, 0, DateTimeKind.Utc);
DateTime localTime = new DateTime(2025, 3, 15, 10, 0, 0, DateTimeKind.Local); // Предположим, UTC+3
Console.WriteLine(utcTime == localTime); // True! Но это неправильно, разница 3 часа!
// А теперь с DateTimeOffset
DateTimeOffset utcOffset = new DateTimeOffset(2025, 3, 15, 10, 0, 0, TimeSpan.Zero);
DateTimeOffset mskOffset = new DateTimeOffset(2025, 3, 15, 13, 0, 0, TimeSpan.FromHours(3));
Console.WriteLine(utcOffset == mskOffset); // True! Так как оба момента времени — 10:00 UTC
3. Сериализация и передача данных
- DateTime: При сериализации в JSON или XML, если Kind не указан, клиент может неправильно интерпретировать время.
- DateTimeOffset: Идеально подходит для API и обмена данными. Смещение всегда передается вместе с временем, что исключает двусмысленность.
4. Работа с SQL
- DateTime: В SQL Server есть тип
datetimeиdatetime2. Они не хранят смещение. - DateTimeOffset: В SQL Server есть тип
datetimeoffset. Он полностью совместим с .NETDateTimeOffsetи хранит смещение.
Когда использовать DateTime, а когда DateTimeOffset?
Вот простая шпаргалка, которая спасет вас от головной боли.
Используйте DateTimeOffset, если:
- Вы храните или передаете абсолютный момент времени (например, время создания записи в логе, дата транзакции).
- Ваше приложение работает с пользователями из разных часовых поясов.
- Вы используете REST API или обмениваетесь данными с другими системами.
- Вы работаете с SQL Server и используете тип
datetimeoffset.
Используйте DateTime, если:
- Вам нужно только локальное время и вы уверены, что контекст часового пояса не важен (например, время открытия магазина, которое всегда одно и то же).
- Вы работаете с устаревшим кодом, который требует именно
DateTime. - Вы храните дату рождения (смещение не имеет