🔷 C#

Работа с датой и временем в C# (.NET): подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представьте себе, что вы — капитан космического корабля, который бороздит просторы Вселенной. У вас на борту есть два главных прибора: хронометр (который показывает текущий момент времени) и бортовой журнал (куда вы записываете события). Без них вы бы потерялись во времени и пространстве: не знали бы, когда взлетели, когда нужно приземлиться, сколько топлива потратили за последний час.

В программировании всё точно так же. Любая программа, которая работает с реальным миром, сталкивается с датами и временем. Это может быть:

  • Календарь задач — напомнить о дне рождения друга через 3 дня.
  • Банковское приложение — показать историю переводов за последний месяц.
  • Игра — посчитать, сколько времени игрок провёл в онлайне.
  • Сервер — записать в лог точное время ошибки.

Без умения работать с датой и временем любая программа будет слепа и глуха к реальному миру. В C# (.NET) для этого есть мощный инструмент — структура DateTime и её друзья. Давайте разберёмся, как они устроены.

Важно: В C# дата и время — это не просто строка вида "12.05.2024". Это специальный тип данных, который хранит время в виде числа (количества тиков) и умеет выполнять с ним математические операции.

Теория: как это работает изнутри

В основе работы с датой и временем в .NET лежит структура DateTime. Она хранит момент времени с точностью до 100 наносекунд (это называется «тик»). Один тик = 100 наносекунд = 0.0001 миллисекунды.

Как это устроено внутри?

Представьте, что у нас есть огромная числовая ось. Начало отсчёта (точка 0) — это 1 января 0001 года в 00:00:00 (полночь). Каждый следующий момент времени — это просто количество тиков, прошедших от этой точки. Например:

  • 1 января 2024 года — это примерно 638 000 000 000 000 тиков от начала отсчёта.
  • Сегодняшний вечер — это ещё плюс несколько миллионов тиков.

Такой подход позволяет:

  • Сравнивать даты (раньше/позже) — просто сравнить два числа.
  • Вычитать даты — получить разницу в виде интервала времени.
  • Добавлять дни, часы, минуты — просто прибавить нужное количество тиков.

Важные структуры и классы:

Тип Описание Пример
DateTime Момент времени (дата + время) 12.05.2024 15:30:00
TimeSpan Промежуток времени (интервал) 3 дня 5 часов 10 минут
DateOnly Только дата (без времени) — появился в .NET 6 12.05.2024
TimeOnly Только время (без даты) — появился в .NET 6 15:30:00
DateTimeOffset Дата и время со смещением относительно UTC 12.05.2024 15:30:00 +03:00
Совет: Всегда используйте DateTimeOffset для хранения дат в базах данных или при передаче между системами. Это убережёт вас от путаницы с часовыми поясами.

Синтаксис в C# (.NET): все формы и варианты

Создание объектов DateTime

Есть несколько способов создать дату и время:

// Способ 1: Текущее время
DateTime now = DateTime.Now;        // Местное время (с учётом часового пояса)
DateTime utcNow = DateTime.UtcNow;  // Всемирное координированное время (UTC)

// Способ 2: Конкретная дата
DateTime birthday = new DateTime(2005, 5, 15); // 15 мая 2005, 00:00:00
DateTime meeting = new DateTime(2024, 12, 25, 14, 30, 0); // 25 декабря 2024, 14:30:00

// Способ 3: Из строки (парсинг)
string dateString = "2024-05-12";
DateTime parsedDate = DateTime.Parse(dateString); // Может выбросить исключение, если строка кривая

// Способ 4: Безопасный парсинг
if (DateTime.TryParse("2024-05-12", out DateTime safeDate))
{
    Console.WriteLine($"Успешно распарсили: {safeDate}");
}
else
{
    Console.WriteLine("Не удалось распарсить дату");
}

// Способ 5: Из тиков
long ticks = 638000000000000000;
DateTime fromTicks = new DateTime(ticks);

Основные свойства и методы

Свойство/Метод Описание Пример
.Year Год (int) 2024
.Month Месяц (1-12) 5
.Day День месяца (1-31) 12
.Hour Часы (0-23) 15
.Minute Минуты (0-59) 30
.Second Секунды (0-59) 45
.DayOfWeek День недели (enum) DayOfWeek.Sunday
.DayOfYear День года (1-366) 133
.AddDays(n) Добавить n дней date.AddDays(5)
.AddMonths(n) Добавить n месяцев date.AddMonths(-2)
.AddYears(n) Добавить n лет date.AddYears(1)
.AddHours(n) Добавить n часов date.AddHours(3.5)
.ToString("формат") Преобразовать в строку по формату date.ToString("dd.MM.yyyy")
.Subtract(otherDate) Вычесть другую дату (результат — TimeSpan) date1.Subtract(date2)

Форматы даты и времени (строковые)

В C# есть два типа форматирования: стандартные и пользовательские.

Стандартные спецификаторы формата:

Спецификатор Описание Пример (для 12.05.2024 15:30:45)
"d" Короткая дата 12.05.2024
"D" Полная дата 12 мая 2024 г.
"t" Короткое время 15:30
"T" Полное время 15:30:45
"f" Полная дата и короткое время 12 мая 2024 г. 15:30
"F" Полная дата и полное время 12 мая 2024 г. 15:30:45
"g" Короткая дата и короткое время 12.05.2024 15:30
"G" Короткая дата и полное время 12.05.2024 15:30:45
"O" ISO 8601 (для передачи данных) 2024-05-12T15:30:45.0000000

Пользовательские форматы (самые популярные):

Шаблон Описание Пример
dd.MM.yyyy День.Месяц.Год 12.05.2024
yyyy-MM-dd Год-Месяц-День (международный) 2024-05-12
dddd, MMMM yyyy День недели, Месяц Год воскресенье, май 2024
HH:mm:ss Часы:Минуты:Секунды (24-часовой) 15:30:45
hh:mm tt Часы:Минуты AM/PM (12-часовой) 03:30 PM

Работа с TimeSpan (промежутками времени)

// Создание TimeSpan
TimeSpan oneDay = new TimeSpan(1, 0, 0, 0); // 1 день
TimeSpan twoHours = new TimeSpan(2, 30, 0); // 2 часа 30 минут
TimeSpan fromDays = TimeSpan.FromDays(3.5); // 3 дня 12 часов

// Свойства TimeSpan
Console.WriteLine(oneDay.TotalHours); // 24
Console.WriteLine(twoHours.Minutes);  // 30
Console.WriteLine(fromDays.TotalDays); // 3.5

// Арифметика с TimeSpan
DateTime start = new DateTime(2024, 1, 1);
DateTime end = start + oneDay; // 2 января 2024
TimeSpan difference = end - start; // 1 день

Примеры кода — от простого к сложному

Пример 1: Приветствие в зависимости от времени суток

// Получаем текущее время
DateTime now = DateTime.Now;

// Извлекаем только часы
int hour = now.Hour;

// Определяем время суток
string greeting;
if (hour >= 5 && hour < 12)
    greeting = "Доброе утро!";
else if (hour >= 12 && hour < 18)
    greeting = "Добрый день!";
else if (hour >= 18 && hour < 23)
    greeting = "Добрый вечер!";
else
    greeting = "Доброй ночи!";

Console.WriteLine($"{greeting} Сейчас {now:HH:mm}");
// Вывод: Добрый день! Сейчас 15:30

Пример 2: Расчёт возраста

// Дата рождения пользователя
DateTime birthDate = new DateTime(2005, 5, 15);
DateTime today = DateTime.Today; // Сегодняшняя дата без времени

// Вычисляем возраст
int age = today.Year - birthDate.Year;

// Проверяем, был ли уже день рождения в этом году
if (today.Month < birthDate.Month || 
    (today.Month == birthDate.Month && today.Day < birthDate.Day))
{
    age--; // Если день рождения ещё не наступил, вычитаем 1 год
}

Console.WriteLine($"Возраст: {age} лет");
// Вывод: Возраст: 19 лет (если сегодня 12.05.2024)

Пример 3: Сколько дней до Нового года?

DateTime today = DateTime.Today;

// Создаём дату следующего Нового года
int nextYear = today.Year + 1;
DateTime newYear = new DateTime(nextYear, 1, 1);

// Вычисляем разницу
TimeSpan daysUntilNewYear = newYear - today;

Console.WriteLine($"До Нового года осталось {daysUntilNewYear.Days} дней");
// Вывод: До Нового года осталось 234 дня

Пример 4: Проверка, является ли год високосным

int year = 2024;

// Используем статический метод DateTime.IsLeapYear
bool isLeap = DateTime.IsLeapYear(year);

Console.WriteLine($"Год {year} {(isLeap ? "високосный" : "не високосный")}");
// Вывод: Год 2024 високосный

// Альтернативный способ: проверить количество дней в феврале
int daysInFebruary = DateTime.DaysInMonth(year, 2);
Console.WriteLine($"В феврале {daysInFebruary} дней");
// Вывод: В феврале 29 дней

Пример 5: Форматирование даты для разных стран

DateTime date = new DateTime(2024, 5, 12, 15, 30, 45);

// Русский формат
string russianFormat = date.ToString("dd.MM.yyyy HH:mm");
Console.WriteLine($"Россия: {russianFormat}");
// Вывод: Россия: 12.05.2024 15:30

// Американский формат
string usFormat = date.ToString("MM/dd/yyyy hh:mm tt");
Console.WriteLine($"США: {usFormat}");
// Вывод: США: 05/12/2024 03:30 PM

// Международный формат (ISO 8601)
string isoFormat = date.ToString("yyyy-MM-ddTHH:mm:ss");
Console.WriteLine($"ISO: {isoFormat}");
// Вывод: ISO: 2024-05-12T15:30:45

// Полное название месяца и дня недели
string fullFormat = date.ToString("dddd, MMMM dd, yyyy");
Console.WriteLine($"Полный: {fullFormat}");
// Вывод: Полный: воскресенье, мая 12, 2024

Пример 6: Работа с часовыми поясами (DateTimeOffset)

// Текущее время в UTC
DateTimeOffset utcNow = DateTimeOffset.UtcNow;
Console.WriteLine($"UTC: {utcNow:yyyy-MM-dd HH:mm:ss zzz}");
// Вывод: UTC: 2024-05-12 12:30:45 +00:00

// Время в Москве (UTC+3)
DateTimeOffset moscowTime = utcNow.ToOffset(TimeSpan.FromHours(3));
Console.WriteLine($"Москва: {moscowTime:yyyy-MM-dd HH:mm:ss zzz}");
// Вывод: Москва: 2024-05-12 15:30:45 +03:00

// Время в Нью-Йорке (UTC-4 летом)
DateTimeOffset newYorkTime = utcNow.ToOffset(TimeSpan.FromHours(-4));
Console.WriteLine($"Нью-Йорк: {newYorkTime:yyyy-MM-dd HH:mm:ss zzz}");
// Вывод: Нью-Йорк: 2024-05-12 08:30:45 -04:00

// Разница между часовыми поясами
TimeSpan diff = moscowTime.Offset - newYorkTime.Offset;
Console.WriteLine($"Разница: {diff.TotalHours} часов");
// Вывод: Разница: 7 часов

Пример 7: Парсинг даты из строки с обработкой ошибок

// Список строк с датами (некоторые некорректные)
string[] dateStrings = {
    "2024-05-12",
    "12.05.2024",
    "May 12, 2024",
    "неправильная дата",
    "2024-13-01", // 13-го месяца не существует
    "2024-02-30"  // 30 февраля не бывает
};

foreach (string str in dateStrings)
{
    // Пытаемся распарсить
    if (DateTime.TryParse(str, out DateTime result))
    {
        Console.WriteLine($"'{str}' -> {result:dd.MM.yyyy}");
    }
    else
    {
        Console.WriteLine($"'{str}' -> НЕ удалось распарсить");
    }
}

/* Вывод:
'2024-05-12' -> 12.05.2024
'12.05.2024' -> 12.05.2024
'May 12, 2024' -> 12.05.2024
'неправильная дата' -> НЕ удалось распарсить
'2024-13-01' -> НЕ удалось распарсить
'2024-02-30' -> НЕ удалось распарсить
*/

Пример 8: Таймер обратного отсчёта (использование TimeSpan)

// Задаём целевую дату (например, день рождения)
DateTime targetDate = new DateTime(2025, 5, 15, 0, 0, 0);
DateTime now = DateTime.Now;

// Вычисляем оставшееся время
TimeSpan remaining = targetDate - now;

// Если дата уже прошла
if (remaining.TotalSeconds < 0)
{
    Console.WriteLine("Этот день уже прошёл!");
}
else
{
    Console.WriteLine($"До дня рождения осталось:");
    Console.WriteLine($"  Дней: {remaining.Days}");
    Console.WriteLine($"  Часов: {remaining.Hours}");
    Console.WriteLine($"  Минут: {remaining.Minutes}");
    Console.WriteLine($"  Секунд: {remaining.Seconds}");
    Console.WriteLine($"  Всего часов: {remaining.TotalHours:F2}");
    Console.WriteLine($"  Всего минут: {remaining.TotalMinutes:F2}");
}

Пример 9: Генерация календаря на месяц

int year = 2024;
int month = 5; // Май

// Первый день месяца
DateTime firstDay = new DateTime(year, month, 1);
// Последний день месяца
DateTime lastDay = new DateTime(year, month, DateTime.DaysInMonth(year, month));

Console.WriteLine($"Календарь на {firstDay:MMMM yyyy}");
Console.WriteLine("Пн Вт Ср Чт Пт Сб Вс");

// Определяем день недели первого дня (0 = воскресенье, 1 = понедельник, ...)
int startDayOfWeek = (int)firstDay.DayOfWeek;
// В русском календаре понедельник = 0, поэтому корректируем
int offset = (startDayOfWeek == 0) ? 6 : startDayOfWeek - 1;

// Печатаем пустые ячейки до первого дня
for (int i = 0; i < offset; i++)
{
    Console.Write("   ");
}

// Печатаем дни месяца
for (int day = 1; day <= lastDay.Day; day++)
{
    Console.Write($"{day,2} ");
    
    // Если суббота (6) — переходим на новую строку
    DateTime currentDay = new DateTime(year, month, day);
    if (currentDay.DayOfWeek == DayOfWeek.Saturday)
    {
        Console.WriteLine();
    }
}

Console.WriteLine(); // Переход на новую строку в конце

Сравнение с Python: чем похоже и чем отличается

Если вы уже знакомы с Python, вам будет легче освоить C#. Давайте сравним основные моменты:

Операция C# (.NET) Python
Текущее время DateTime.Now datetime.now()
Текущее UTC DateTime.UtcNow datetime.utcnow()
Создание даты new DateTime(2024, 5, 12) datetime(2024, 5, 12)
Разница между датами date1 - date2 (TimeSpan) date1 - date2 (timedelta)
Добавление дней date.AddDays(5) date + timedelta(days=5)
Форматирование date.ToString("dd.MM.yyyy") date.strftime("%d.%m.%Y")
Парсинг строки DateTime.Parse("2024-05-12") datetime.strptime("2024-05-12", "%Y-%m-%d")
Только дата DateOnly (.NET 6+) date (из datetime)
Только время TimeOnly (.NET 6+) time (из datetime)
Часовые пояса DateTimeOffset, TimeZoneInfo pytz или zoneinfo (Python 3.9+)

Главные отличия:

  1. Типизация: В C# всё строго типизировано. Вы не сможете сложить DateTime и число напрямую — нужно использовать методы AddDays, AddHours и т.д. В Python можно писать date + timedelta(days=1).
  2. Безопасность: В C# есть TryParse, который не выбрасывает исключение при ошибке парсинга. В Python нужно использовать try-except.
  3. Производительность: C# работает быстрее при массовых операциях с датами (например, обработка миллионов записей).
  4. Часовые пояса: В C# встроенная поддержка часовых поясов через TimeZoneInfo и DateTimeOffset. В Python нужно подключать дополнительные библиотеки.

Типичные ошибки и подводные камни

Ошибка 1: Путаница между DateTime.Now и DateTime.UtcNow

Проблема: Если вы сохраняете DateTime.Now в базу данных, а потом читаете её с сервера в другом часовом поясе, время будет неверным.
Решение: Всегда храните время в UTC (DateTime.UtcNow), а преобразуйте в локальное только при отображении пользователю.

Ошибка 2: Игнорирование культуры при парсинге

// Проблема: В США даты пишут как "05/12/2024" (месяц/день/год)
// А в России — "12.05.2024" (день/месяц/год)
string usDate = "05/12/2024";
DateTime parsed = DateTime.Parse(usDate); // Может дать 12 мая или 5 декабря!

// Решение: Указывать культуру
using System.Globalization;

string russianDate = "12.05.2024";
DateTime correct = DateTime.Parse(russianDate, new CultureInfo("ru-RU"));

Ошибка 3: Выход за границы месяца

// Проблема: Добавление месяца к 31 января даёт 31 февраля — ошибка!
DateTime jan31 = new DateTime(2024, 1, 31);
DateTime feb31 = jan31.AddMonths(1); // Ошибка! 31 февраля не существует

// Решение: Использовать метод, который корректирует дату
// В C# AddMonths автоматически корректирует: 31.01 + 1 месяц = 29.02 (високосный год)
Console.WriteLine(feb31); // 29.02.2024

Ошибка 4: Сравнение дат без учёта времени

// Проблема: Сравнение двух DateTime, где у одного есть время, а у другого нет
DateTime date1 = new DateTime(2024, 5, 12); // 00:00:00
DateTime date2 = DateTime.Now; // 12.05.2024 15:30:45

if (date1 == date2) // False! Потому что время разное
{
    Console.WriteLine("Даты равны");
}

// Решение: Сравнивать только дату
if (date1.Date == date2.Date)
{
    Console.WriteLine("Даты равны"); // True
}

Ошибка 5: Неправильное форматирование для разных культур

// Проблема: Использование ToString() без указания культуры
DateTime date = new DateTime(2024, 5, 12);
Console.WriteLine(date.ToString()); // Зависит от настроек системы!

// Решение: Всегда указывать формат
Console.WriteLine(date.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture));

Ошибка 6: Забыли про високосные годы

// Проблема: Ручной расчёт количества дней в году
int daysInYear = 365; // Неправильно для високосного года!

// Решение: Использовать встроенные методы
int year = 2024;
int days = DateTime.IsLeapYear(year) ? 366 : 365;
// Или ещё проще:
DateTime lastDay = new DateTime(year, 12, 31);
int correctDays = lastDay.DayOfYear; // 366 для 2024

Лучшие практики и советы профессионалов

  1. Всегда используйте UTC для хранения. Преобразуйте в локальное время только при отображении пользователю. Это избавит вас от головной боли с часовыми поясами.
  2. Используйте DateTimeOffset вместо DateTime для данных, которые могут пересекать границы часовых поясов (например, веб-API, базы данных).
  3. Для .NET 6+ используйте DateOnly и TimeOnly, если вам не нужна полная дата и время. Это делает код понятнее и производительнее.
  4. Избегайте DateTime.Now в циклах. Если вам нужно много раз получить текущее время, сохраните его в переменную один раз:
// Плохо: каждый вызов DateTime.Now создаёт новый объект
for (int i = 0; i < 1000; i++)
{
    Console.WriteLine(DateTime.Now);
}

// Хорошо: сохраняем время один раз
DateTime now = DateTime.Now;
for (int i = 0; i < 1000; i++)
{
    Console.WriteLine(now);
}
  1. Используйте TryParse вместо Parse при работе с пользовательским вводом. Это предотвратит исключения.
  2. Для высокоточных измерений используйте Stopwatch из System.Diagnostics, а не DateTime:
using System.Diagnostics;

Stopwatch stopwatch = Stopwatch.StartNew();

// Какой-то код, который нужно замерить
Thread.Sleep(1000); // Ждём 1 секунду

stopwatch.Stop();
Console.WriteLine($"Прошло: {stopwatch.ElapsedMilliseconds} мс");
// Вывод: Прошло: 1000 мс (примерно)
  1. Не изобретайте велосипед. .NET предоставляет огромное количество методов для работы с датами. Прежде чем писать свою логику, проверьте, нет