Какво не е наред със структурата DateTime?

Забележки:1. В предишната бележка преведох "time zone" като "часова зона", тъй като ставаше дума за часови зони в САЩ, които имат конкретно име. В този случай е по-правилно да използвате "часовата зона". Тук е използван по-правилен превод.

2. Малко вмъкване от Wikipedia ще ви даде представа какво е UTC и как се различава от GMT -

Координираното универсално време (UTC) е стандартът, по който обществото регулира часовниците и часа. Различава се с цял брой секунди от атомното време и с частичен брой секунди от UTC UT1.

Въведено е UTC, за да замени остарялото време по Гринуич (GMT). Новата скала за време UTC беше въведена, тъй като скалата GMT е неравномерна скала и е свързана с дневното въртене на Земята. UTC скалата се основава на Единната атомна времева скала (TAI) и е по-удобна за цивилна употреба.

Часовите зони по света се изразяват като положителни и отрицателни отмествания спрямо UTC.

Моля, имайте предвид, че UTC времето не се превежда през зимата или лятото. Следователно за тези места, където има преход към лятно часово време, изместването спрямо UTC се променя.

Сега нека продължим да се занимаваме със структурите, които обслужват обекти като дата и час.

Какво означава DateTime?

Когато попадна на въпрос в Stack Overflow, който казва, че DateTime не прави това, което трябва да прави, често се оказвам в състояние на съзерцание - какво точно е трябвало да представлява определената стойност? Отговорът е прост - дата и час, нали? Но всичко е много по-сложно, веднага щом започнете да се справяте с проблема по-внимателно. Например,Да приемем, чечасовникът не е тиктакал между извикванията на дветесвойства в кода по-долу. И така, каква стойност ще приеме променливата "мистерия" като резултат?

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

Честно казано не знам до какво ще доведе изпълнението на този код. Има три версии, всяка от които има своя повече или по-малко разумна обосновка:

  • Стойността винаги ще бъдеtrue: две стойности са свързани с една и съща точка във времето, само едната е локално изразена, а другата е универсална
  • Стойността винаги ще бъдеfalse: двете стойности представляват две различни стойности за дата, т.е. автоматично неравен
  • И още веднъжtrue- ако вашата местна часова зона е синхронизирана с UTC (координирано универсално време), т.е. когато часовите зони изобщо не се вземат предвид - и двете стойности ще бъдат равни
Не ме интересува много кой отговор е правилен - неочевидността на логиката на поведението на кода е знак за по-дълбоки проблеми. Всъщност всичко се връща към свойството DateTime.Kind, което позволява на DateTime да представя три типа стойности:

DateTimeKind.Utc: UTC дата и часDateTimeKind.Local: дата и час локални за системата, на която се изпълнява кодътDateTimeKind.Unspecified: Ммм, сложно. Зависи какво правиш с него.

Стойността на свойството има различен ефект върху различните операции. Например, ако приложите метода ToUniversalTime() към „неопределена“ стойност на DateTime, методът ще приеме, че се опитвате да конвертирате локална стойност. От друга страна, ако приложите метода ToLocalTime() към „неуточнена“ стойност на DateTime, тогава се прави предположението, чепървоначално сте имали стойността като UTC. Това е един модел на поведение.

Ако създадете DateTimeOffset от DateTime и TimeSpan, поведението е малко по-различно:

  • с UTC стойността всичко е просто - предаваме UTC, искаме да получим представянето на "UTC + посоченото отместване"
  • локалната стойност е вярна само понякога: конструкторът проверява дали отместването от UTC в определеното местно време в часовата зона по подразбиране на системата съвпада с посоченото от вас отместване
  • неуточнена стойност ("неуточнена") винаги е вярна и представлява местното време в някаква неуточнена часова зона, така че отместването да е валидно по това време.
Не знам за вас, но това състояние на нещата предизвиква у мен лека семантична истерия. Това е като да имате "числов" тип, който съдържа поредица от цифри, но трябва да използвате различно свойство, за да разберете дали цифрата е десетична или шестнадесетична и отговорът понякога е "Е, какво мислите?".

Разбира се, в .NET 1.1 свойството DateTimeKind изобщо липсваше. Това не означава, че проблемът не е съществувал. Това означава, че объркващо поведение, което се опитва да осмисли тип, който съдържа различни видове стойности и не се опитва да бъде по никакъв начин последователен. Базира се на предположението, че стойността на датата е постоянно неопределена.

Използването на структурата 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) и съответно има свои собствени идентификатори. Може би сте ги виждали - "Европа / Лондон" или "Америка / Лос_Анджелес" - това са идентификаторите на Олсън. Работете с уеб услуга, която предоставя геоинформация - има шансове да използва Olson IDs. Опитайте друга календарна система - има вероятност тя също да използва идентификатори на Olson. Тук също има проблеми. Например със стабилността на идентификатора, която Unicode Consortium се опитва да разреши с CLDR... но поне имате добър шанс. Би било страхотно, ако TimeZoneInfo предложи някакъв начин за картографиране на двете схеми за идентификатори или ще бъде приложен някъде другаде в .NET. (Noda Time знае и за двата комплектаидентификатори, въпреки че картографирането (картографирането) за всички все още не е налично. Това ще бъде коригирано преди окончателното издание.)

Второ, този клас се основава на използването на 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 - тогава трябва да използвате тези типове във вашия код с изключително внимание и прецизност. И само това ще ме направи щастлив.

(Е, ако сте прочели този опус до края, това означава, че не съм си губил времето за превод -бележка на преводача).

Много свободен превод (c) Alien V.F. известен още като hDrummer, оригиналът е тук.