Що ж не так зі структурою DateTime

Примітки:1. У попередній замітці "time zone" я переклав як «тимчасова зона», оскільки йшлося про часові пояси США, що мають специфічну назву. У разі коректніше використовувати " часовий пояс " . Тут використовується коректніший переклад.

2. Невелика врізка з Вікіпедії дасть вам розуміння, що таке UTC і чим воно відрізняється від GMT.

Всесвітній координований час (UTC) — стандарт, за яким суспільство регулює годинник і час. Відрізняється цілу кількість секунд від атомного часу і дробову кількість секунд від всесвітнього часу UT1.

UTC було введено замість застарілого середнього часу за Грінвічем (GMT). Нова шкала часу UTC була введена, оскільки шкала GMT є нерівномірною шкалою і пов'язана із добовим обертанням Землі. Шкала UTC заснована на рівномірній шкалі атомного часу (TAI) і є зручнішою для цивільного використання.

Годинні пояси навколо земної кулі виражаються як позитивне та негативне зміщення від UTC.

Слід пам'ятати, що час UTC не переводиться ні взимку, ні влітку. Тому для тих місць, де є перехід на літній час, змінюється зсув щодо UTC.

Тепер продовжимо розбиратися зі структурами, які обслуговують такі сутності, як дата та час.

Що означає DateTime?

Коли я натикаюсь на сайті Stack Overflow на запитання, в якому йдеться про те, що DateTime не робить того, чого від нього очікують, я часто виявляю себе в стані роздумів — що конкретно повинно було представляти вказане значення? Відповідь проста - дату і час, так? Але все набагато складніше, як тільки починаєш розбиратися з проблемою ретельніше. Наприклад,припустимо, щогодини не тицьнули між викликами двох властивостейу наведеному нижче коді. Тож яке значення набуде в результаті змінна «mystery»?

DateTime utc = DateTime.UtcNow; DateTime local = DateTime.Now; bool mystery = local == utc;

Я, чесно, не знаю, до чого приведе виконання цього коду. Є три версії, кожна з яких має своє більш-менш обґрунтоване:

  • Значення завжди буде рівнимtrue: два значення асоційовані з одним і тим же моментом у часі, тільки одне виражено локально, а друге універсально
  • Значення завжди буде рівнимfalse: два значення представляють два різних значення дат, тобто. автоматично нерівні
  • І ще разtrue— якщо ваш локальний часовий пояс синхронізовано з UTC (Світовим координованим часом), тобто. тоді, коли часові пояси взагалі не беруться до уваги - обидва значення будуть рівні
Мене мало хвилює, яка відповідь правильна — неочевидність логіки поведінки коду є ознакою глибших проблем. Насправді все повертається до властивості DateTime.Kind, яка дозволяє DateTime представляти три типи значень:

DateTimeKind.Utc: дату та час UTCDateTimeKind.Local: дату та час, локальні для даної системи, в якій виконується кодDateTimeKind.Unspecified: М-м-м, хитро. Залежить від того, що ви з ним робите.

Значення якості впливає різні операції. Наприклад, якщо ви застосовуєте метод ToUniversalTime() до «unspecified» значення DateTime, метод зробить припущення, що ви намагаєтеся конвертувати локальне значення. З іншого боку, якщо ви застосовуєте метод ToLocalTime() до «unspecified» значення DateTime, то буде зроблено припущення про те, щоспочатку у вас було значення як UTC. Це одна модель поведінки.

Якщо ж ви створюєте DateTimeOffset із DateTime та TimeSpan, поведінка дещо відрізняється:

  • зі значенням UTC все просто - передаємо UTC, хочемо отримати уявлення «UTC + вказане зміщення»
  • локальне значення вірне тільки іноді: конструктор перевіряє збіг зсуву від UTC у вказаному локальному часі в часовому поясі, що використовується системою за умовчанням, зі зміщенням, вказаним вами
  • невказане значення («unspecified») завжди правильне і є локальне час у деякому невказаному часовому поясі отже зсув є вірним у цей час.
Не знаю, як у вас, а у мене такий стан справ викликає легку семантичну істерику. Це все одно, що мати «числовий» тип, який містить послідовність цифр, але ви повинні використовувати іншу властивість для того, щоб дізнатися ця ця десяткова чи шістнадцяткова, а відповіддю іноді буде — «Ну, а як ти сам думаєш?».

Звичайно, в .NET 1.1 властивість DateTimeKind взагалі була відсутня. Це не означає, що й проблеми не існувало. Це означає, що поведінка, що збиває з пантелику і намагається надати сенс типу, що зберігає різні види значень і не намагалося бути скільки-небудь послідовним. Воно базувалося на припущенні, що значення дати перманентно має вигляд Unspecified.

Чи вирішує проблему використання структури DateTimeOffset?

Добре. Тепер ми знаємо, що нам не дуже подобається тип DateTime. Чи допоможе нам DateTimeOffset? Так, частково. Значення типу DateTimeOffset несе чітке значення: воно містить локальні дату і час із зазначеним усуненням від UTC. Можливо тепер я маю зробити відступ і пояснити вам, що я маю на увазі під «локальними»датою та часом, а також (тимчасовими) моментами.

(Ви ще тут? Продовжимо — сподіваюся, що попередній параграф був найважчим у цій замітці. Хоча в ньому описано дуже важливу концепцію.)

Отже, DateTimeOffset відноситься до (глобального) моменту часу, але також працює з локальною датою та часом. Це означає, що ця структура не є ідеальним типом для представлення локальних дат і часів — але такою не є і DateTime. DateTime з встановленим властивістю DateTimeKind.Local насправді не є локальним з тих же міркувань - вона прив'язана до часового поясу, встановленого за умовчанням у системі, в якій вона використовується. DateTime виду DateTimeKind.Unspecified підходить трохи краще в окремих випадках - наприклад, при створенні DateTimeOffset - але семантика виглядає дивною в інших випадках, що описано вище. У результаті ні DateTimeOffset ні DateTime не є добрими типами для відображення дійсно локальних дати та часу.

Структура DateTimeOffset також не є хорошим вибором для прив'язки до конкретного часового поясу, оскільки вона не має поняття, який часовий пояс дав відповідне зміщення першим. У .NET 3.5 є цілком адекватний клас TimeZoneInfo, але немає типу, який говорить про «локальний час у конкретному часовому поясі». Тому маючи змінну типу DateTimeOffset, ви знаєте конкретний час у деякому часовому поясі, але ви не знаєте, яким буде локальний час через хвилину, оскільки зміщення для цього поясу могло змінитися (зазвичай завдяки переходу на літній/зимовий час).

Як щодо дат та часу?

До цього ми обговорювали лише значення, що зберігають «дату та час». А що можна сказати про типи, в яких зберігаються або тільки дата або лише час? Найчастіше, звичайно,необхідно зберігати лише дату, але бувають і випадки, коли необхідно зберігати лише час.

І таки так, ви можете використовувати тип DateTime для зберігання дати - чорт, та є навіть властивість DateTime.Date, що повертає дату для конкретних дати і часу ... але тільки у вигляді іншого значення типу DateTime з часом, встановленим опівночі. Це не те саме, що мати окремий тип, який легко ідентифікувати як «тільки дата» (або «тільки час» — .NET використовує TimeSpan для цього, що знову не здається мені зовсім правильним).

А що ж власне з часовими поясами? Тут TimeZoneInfo виглядає цілком пристойно.

Як я вже сказав, TimeZoneInfo не поганий. Щоправда, він має дві великі проблеми і трохи менший.

По-перше, за основу взято ідентифікатори часових поясів Windows. З одного боку логічно, а з іншого — це не те, що використовує решту світу. Усі не-Windows системи, які я бачив, використовують базу даних часових поясів Олсона (Olson) (також відомої як tz або zoneinfo), відповідно в ній є свої ідентифікатори. Можливо, ви їх бачили — «Europe/London» або «America/Los_Angeles» — це ідентифікатори Олсона. Попрацюйте з веб-сервісом, що надає геоінформацію, — є шанси, що він використовує ідентифікатори Олсона. Попрацюйте з іншою календарною системою — є шанси, що вона використовує ідентифікатори Олсона. Тут також є свої проблеми. Наприклад, зі стабільністю ідентифікаторів, які Unicode Consortium намагається вирішити за допомогою CLDR… але принаймні у вас є добрий шанс. Було б чудово, якби TimeZoneInfo пропонувала якийсь спосіб встановлення відповідності між двома схемами ідентифікаторів або це було б реалізовано ще десь у .NET. (Noda Time знає про обидва набориідентифікаторів, хоча відповідність (mapping) для всіх поки що недоступна. Це буде виправлено перед фінальним випуском.)

По-друге, цей клас полягає в використанні DateTime і DateTimeOffset, тобто. ви повинні бути обережними при його використанні — якщо ви встановите один вид DateTime, а передаєте інший, то у вас можуть бути проблеми. Клас досить добре документований, але, чесно кажучи, пояснення таких речей за своєю суттю досить непросто без заплутування ситуації за допомогою використання суперечливих термінів.

Є проблеми і з двозначними чи хибними значеннями локальних дат та часу. Вони виникають при переході на літню/зимову пору: якщо час переводять вперед (наприклад, з 1:00 на 2:00), то є шанс отримання невірного локального часу (наприклад, цього дня 1:30 так і не настане). Якщо годинник переводиться назад (наприклад, з 2:00 на 1:00), це призводить до двозначності: 1.30 трапляється двічі. Ви можете явно уточнити у TimeZoneInfo, коли конкретне значення є невірним або двозначним, але легко просто забути про таку можливість. Якщо ви спробуєте конвертувати локальний час під час UTC за допомогою часового поясу, буде згенеровано виняток, якщо час неправильний. А ось двозначний час буде прийнято за стандартний за умовчанням (а не як літній час). Такі рішення не дозволяють розробникам навіть врахувати використані особливості. Говорити про які…

Зараз ви можете подумати: «Роздув з мухи слона. Я не хочу думати про це – навіщо ти намагаєшся настільки все ускладнити? Я використовував .NET API роками і не мав проблем». Якщо ви так подумали, я можу запропонувати три варіанти:

  • Ви набагато розумніші за мене, і розумієте всі ці складності на інтуїтивному рівні. Ви завждивикористовуєте правильний вигляд для змінної типу DateTime; там, де треба - використовуєте DateTimeOffset і завжди чините правильно з некоректними або двозначними локальними датою/часом. Без жодних сумнівів, ви також пишете багатопоточний код, що не блокується, з спільним доступом до стану об'єкта найефективнішим і в той же час надійним способом. То якого біса ви все це читаєте, дозвольте дізнатися?
  • Ви стикалися з цими проблемами, але здебільшого забули про них - зрештою вони забрали всього 10 хвилин вашого життя, поки ви експериментували над отриманням прийнятного результату (або принаймні для проходження тесту; хоча такі тести цілком могли бути концептуально неправильні) . Можливо, ви були спантеличені, зіткнувшись з цією проблемою, але вирішили, що проблема у вас, а не в API.
  • Ви не стикалися з цією проблемою, оскільки вважаєте тестування коду нудним заняттям, оскільки він працює в одному часовому поясі, на комп'ютерах, які завжди вимкнені вночі (тобто не піддаються впливу переходів на зимовий/літній час). Ви, певною мірою, щасливчик, але все-таки ви забуваєте про часовий пояс.
Жарти жартами, але проблема справді реальна. І якщо ви ніколи раніше не замислювалися про різницю між «локальним» часом та «глобальним» моментом до цієї замітки, то ось ви це й зробили. Це важлива відмінність — воно подібне до різниці між двійковими числами з плаваючою точкою і десятковими числами з плаваючою точкою. Помилки можуть бути не очевидними, важкими в діагностиці, погано зрозумілими, погано коригуються і знову виникають в іншому місці програми.

Все це, звичайно, лежить на поверхні актуальних бізнес-правил, які ви намагаєтеся реалізувати. А вони також можуть бути непростими. З урахуванням усіх цихскладнощів, ви як мінімум повинні мати API, який дозволить вам відносно ясно висловити те, що ви маєте на увазі.

Так чи ідеальний Noda Time?

Як не дивно, для всіх буде краще, якщо команда, що працює над BCL, візьме до уваги цю статтю і вирішить радикально переробити API для .NET 6 (я припускаю, що корабель ". NET 5" вже спущений на воду). І в той час, поки я займаюся цим, я впевнений, що є багато інших проектів, які принесуть мені задоволення — відверто кажучи, питання дат і часу надто важливі для .NET спільноти, щоб тривалий час лежати виключно на моїх плечах.

Сподіваюся, що я переконав вас у тому, що у .NET API є суттєві недоліки. Можливо, я також переконав вас у тому, що Noda Time варта ближчого знайомства, але не це було головною метою. Якщо ви дійсно розумієте недоліки вбудованих типів – особливо семантичну двозначність DateTime – то повинні використовувати ці типи у вашому коді з особливою обережністю та акуратністю. І вже це зробить мене щасливим.

(Ну а якщо ви дочитали цей опус до кінця, значить і я недаремно витратив свій час на переклад -прим.переводчика).

Дуже вільний переклад (с) Чужа В.Ф. aka hDrummer, оригінал тут.