Время — штука относительная. Особенно в коде
Представьте: вы пишете приложение для бронирования авиабилетов. Пользователь из Нью-Йорка выбирает рейс в 14:00 по местному времени, а сервер в Лондоне сохраняет это как DateTime.Now. Что пойдет не так? Абсолютно всё. В лучшем случае — билет на три часа раньше, в худшем — пользователь пропустит рейс и оставит гневный отзыв.
Работа с временными зонами в C# — это та тема, которую многие разработчики откладывают «на потом». Но именно она отделяет любительский код от профессионального. Сегодня мы разберемся, как подружить ваше приложение с часовыми поясами, не сойти с ума от летнего времени и не потерять ни одного пользователя.
DateTime vs DateTimeOffset: первый бой с реальностью
Многие новички в C# используют DateTime для всего подряд. И это нормально, пока вы не сталкиваетесь с глобальными пользователями. Проблема в том, что DateTime сам по себе не знает, в какой временной зоне он находится. Это просто «момент во времени» без контекста.
// Плохо: время без контекста
DateTime localTime = DateTime.Now; // 14:00 — но где?
DateTime utcTime = DateTime.UtcNow; // уже лучше, но всё равно не идеально
// Хорошо: время со смещением
DateTimeOffset withOffset = DateTimeOffset.Now; // 14:00 +03:00
Console.WriteLine(withOffset); // 06.04.2025 14:00:00 +03:00
DateTimeOffset — ваш первый шаг к осознанному управлению временем. Он хранит не только дату и время, но и смещение относительно UTC. Это спасает, когда нужно передать данные между системами или просто показать пользователю корректное локальное время.
Когда DateTime всё-таки нужен?
Честно? Почти никогда для хранения моментов времени. Если вы пишете код для локального desktop-приложения — можно. Но как только появляется база данных, API или несколько пользователей — переходите на DateTimeOffset или DateTime с явным указанием DateTimeKind.Utc.
TimeZoneInfo: ваш личный атлас часовых поясов
В .NET есть мощный класс TimeZoneInfo. Он позволяет работать с любыми временными зонами, которые известны операционной системе. И их сотни — от стандартных UTC+0 до экзотических зон вроде «Lord Howe Island» с 30-минутным смещением.
// Получаем список всех доступных зон
ReadOnlyCollection<TimeZoneInfo> allZones = TimeZoneInfo.GetSystemTimeZones();
foreach (var zone in allZones.Take(5))
{
Console.WriteLine($"{zone.Id}: {zone.DisplayName}");
}
// Вывод (пример):
// UTC: (UTC) Coordinated Universal Time
// Russian Standard Time: (UTC+03:00) Moscow, St. Petersburg
// Eastern Standard Time: (UTC-05:00) Eastern Time (US & Canada)
Конвертация времени между зонами
Допустим, пользователь из Москвы хочет узнать, во сколько начнется вебинар по Лондону. Вот как это сделать правильно:
// Исходное время — московское
DateTime moscowTime = new DateTime(2025, 6, 15, 18, 0, 0);
TimeZoneInfo moscowZone = TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time");
TimeZoneInfo londonZone = TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time");
// Конвертация
DateTime londonTime = TimeZoneInfo.ConvertTime(moscowTime, moscowZone, londonZone);
Console.WriteLine($"В Москве: {moscowTime}, в Лондоне: {londonTime}");
// В Москве: 15.06.2025 18:00:00, в Лондоне: 15.06.2025 16:00:00
Обратите внимание: мы явно указываем исходную и целевую зоны. Это исключает двусмысленность. TimeZoneInfo.ConvertTime сам учтет разницу во времени, включая летнее/зимнее.
Летнее время (DST): главный враг разработчика
Два раза в год мир сходит с ума. В марте часы переводят вперед, в октябре — назад. И если ваше приложение не готово к DST, пользователи увидят либо «лишний» час, либо пропущенный. C# справляется с этим автоматически — если вы используете правильные инструменты.
// Пример: переход на летнее время в США
TimeZoneInfo easternZone = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
// Дата перехода: 9 марта 2025, 2:00 AM -> 3:00 AM
DateTime ambiguousTime = new DateTime(2025, 3, 9, 2, 30, 0, DateTimeKind.Unspecified);
// Проверяем, является ли время неоднозначным (ambiguous)
if (easternZone.IsAmbiguousTime(ambiguousTime))
{
Console.WriteLine("Это время существует дважды (при переходе назад)");
}
// Проверяем, является ли время недействительным (invalid)
if (easternZone.IsInvalidTime(ambiguousTime))
{
Console.WriteLine("Это время не существует (при переходе вперед)"); // Выведет это!
}
Ключевой совет: всегда храните время в UTC, а на клиенте конвертируйте в локальное. Так DST перестанет быть проблемой — сервер просто не знает о «скачках» времени.
Практический кейс: веб-приложение с глобальной аудиторией
Представьте, что вы пишете систему для онлайн-курсов. Уроки начинаются в определенное время, и студенты из разных стран должны видеть корректное расписание. Вот как это организовать:
public class CourseService
{
// Храним время начала в UTC
public DateTimeOffset GetLessonStartUtc(int lessonId)
{
// В реальности — запрос к БД
return new DateTimeOffset(2025, 6, 20, 15, 0, 0, TimeSpan.Zero); // 15:00 UTC
}
// Конвертируем для конкретного пользователя
public DateTimeOffset GetLessonStartForUser(int lessonId, string userTimeZoneId)
{
var utcTime = GetLessonStartUtc(lessonId);
var userZone = TimeZoneInfo.FindSystemTimeZoneById(userTimeZoneId);
// TimeZoneInfo.ConvertTimeBySystemTimeZoneId умеет работать с DateTimeOffset
return TimeZoneInfo.ConvertTime(utcTime, userZone);
}
// Показываем удобный формат
public string GetFormattedLessonTime(int lessonId, string userTimeZoneId)
{
var userTime = GetLessonStartForUser(lessonId, userTimeZoneId);
return userTime.ToString("dd MMM yyyy, HH:mm 'по местному времени'");
}
}
// Использование
var service = new CourseService();
string message = service.GetFormattedLessonTime(42, "Eastern Standard Time");
Console.WriteLine(message); // 20 Jun 2025, 11:00 по местному времени
Обратите внимание: мы нигде не используем DateTime.Now. Вся логика строится вокруг UTC и явного указания временной зоны пользователя. Идентификатор зоны ("Eastern Standard Time") можно получить через JavaScript на клиенте (Intl.DateTimeFormat().resolvedOptions().timeZone) или из настроек профиля.
Сериализация и JSON: как не потерять время
При передаче времени через API (JSON) часто возникает путаница. Стан