Работа с временными зонами в C#: как не опоздать на встречу с сервером

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Время — штука относительная. Особенно в коде

Представьте: вы пишете приложение для бронирования авиабилетов. Пользователь из Нью-Йорка выбирает рейс в 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) часто возникает путаница. Стан

Блоги

Рекомендаций по книгам