Najlepsze rozwiązania dotyczące kodowania przy użyciu daty/godziny w .NET Framework

 

Dan Rogers
Microsoft Corporation

Luty 2004 r.

Dotyczy
   Microsoft® .NET Framework
   Microsoft® ASP.NET Web Services
   Serializacja XML

Krótki opis: Pisanie programów, które przechowują, wykonują obliczenia i serializują wartości czasu przy użyciu typu DateTime w usłudze Microsoft .NET Framework wymaga znajomości różnych problemów związanych z reprezentacjami czasu dostępnymi w systemach Windows i .NET. Ten artykuł koncentruje się na kluczowych scenariuszach testowania i programowania obejmujących czas i definiuje zalecenia dotyczące najlepszych rozwiązań dotyczących pisania programów korzystających z typu DateTime w firmie Microsoft. Aplikacje i zestawy oparte na platformie NET. (18 drukowanych stron)

Zawartość

Tło
   Co to jest data/godzina, mimo to?
   Reguły
Strategie magazynowania
   Najlepsze rozwiązanie nr 1
   Najlepsze rozwiązanie nr 2
Wykonywanie obliczeń
   Nie daj się oszukać ponownie
   Najlepsze rozwiązanie nr 3
   Sortowanie metod DateTime
Specjalny przypadek XML
   Najlepsze rozwiązanie nr 4
   Najlepsze rozwiązanie nr 5
Klasa Coders Quandary
   Najlepsze rozwiązanie nr 6
Obsługa czasu letniego
   Najlepsze rozwiązanie nr 7
Formatowanie i analizowanie wartości User-Ready
   Przyszłe zagadnienia
Problemy z metodą DateTime.Now()
   Najlepsze rozwiązanie nr 8
Kilka mało znanych dodatków
Podsumowanie

Tło

Wielu programistów napotyka przypisania, które wymagają ich dokładnego przechowywania i przetwarzania danych zawierających informacje o dacie i godzinie. Na pierwszy rzut oka typ danych środowiska uruchomieniowego języka wspólnego (CLR) DateTime wydaje się być idealny dla tych zadań. Nie jest to jednak rzadkością dla programistów, ale bardziej prawdopodobnych testerów, aby napotkać przypadki, w których program po prostu traci śledzenie prawidłowych wartości czasu. Ten artykuł koncentruje się na problemach związanych z logiką obejmującą datę/godzinę i w ten sposób odkrywa najlepsze rozwiązania dotyczące pisania i testowania programów, które przechwytują, przechowują, pobierają i przesyłają informacje o dacie/dacie.

Co to jest data/godzina, mimo to?

Gdy przyjrzymy się dokumentacji biblioteki klas platformy NET Framework, zobaczymy, że "Typ wartości CLR System.DateTime reprezentuje daty i godziny od 12:00:00 północy, 1 stycznia 0001 ad do 11:59:59 pm, grudzień 31 99999 AD." Czytając dalej, dowiadujemy się, nic dziwnego, że wartość DateTime reprezentuje chwilę w czasie i że powszechną praktyką jest rejestrowanie wartości punkt w czasie w uniwersalnym czasie koordynowanym (UCT) — powszechnie znany jako Greenwich Mean Time (GMT).

Na pierwszy rzut oka programista odkrywa, że typ DateTime jest całkiem dobry w przechowywaniu wartości czasu, które mogą wystąpić w bieżących problemach programistycznych, takich jak w aplikacjach biznesowych. Mając to pewność, wielu nie podejrzewających programistów rozpoczyna kodowanie, mając pewność, że mogą nauczyć się jak najwięcej czasu, gdy idą do przodu. To podejście "learn-as-you-go" może prowadzić do kilku problemów, więc zacznijmy je identyfikować. Są one różne od problemów w dokumentacji po zachowania, które należy uwzględnić w projektach programu.

Dokumentacja wersji 1.0 i 1.1 dla system.DateTime udostępnia kilka uogólnień, które mogą odrzucać niczego nie podejrzewającego programistę. Na przykład w dokumentacji nadal mówi się, że metody i właściwości znalezione w klasie DateTime zawsze używają założenia, że wartość reprezentuje lokalną strefę czasową komputera lokalnego podczas wykonywania obliczeń lub porównań. Ta uogólnienie okazuje się nieprawdziwe, ponieważ istnieją pewne typy obliczeń daty i godziny, które zakładają GMT, a inne, które zakładają lokalny widok strefy czasowej. W dalszej części tego artykułu wskazano te obszary.

Zacznijmy więc od eksplorowania typu DateTime, przedstawiając szereg reguł i najlepszych rozwiązań, które mogą pomóc w prawidłowym działaniu kodu po raz pierwszy.

Reguły

  1. Obliczenia i porównania wystąpień typu Data/godzina mają znaczenie tylko wtedy, gdy porównywane lub używane wystąpienia są reprezentacjami punktów w czasie z tej samej perspektywy strefy czasowej.
  2. Deweloper jest odpowiedzialny za śledzenie informacji o strefie czasowej skojarzonych z wartością DateTime za pośrednictwem pewnego mechanizmu zewnętrznego. Zazwyczaj jest to realizowane przez zdefiniowanie innego pola lub zmiennej używanej do rejestrowania informacji o strefie czasowej podczas przechowywania typu wartości DateTime. Takie podejście (przechowywanie czujnika strefy czasowej obok wartości DateTime) jest najbardziej dokładne i umożliwia różnym deweloperom w różnych punktach cyklu życia programu zawsze jasne zrozumienie znaczenia wartości DateTime. Innym typowym podejściem jest uczynienie go "regułą" w projekcie, że wszystkie wartości czasu są przechowywane w określonym kontekście strefy czasowej. Takie podejście nie wymaga dodatkowego magazynu w celu zapisania widoku kontekstu strefy czasowej użytkownika, ale wprowadza ryzyko, że wartość czasu zostanie błędnie zinterpretowana lub zapisana niepoprawnie w dół drogi przez dewelopera, który nie zna reguły.
  3. Wykonywanie obliczeń daty i godziny na wartościach reprezentujących czas lokalny komputera może nie zawsze zwracać prawidłowy wynik. Podczas wykonywania obliczeń na wartościach czasu w kontekstach strefy czasowej, które ćwiczą czas letni, należy przekonwertować wartości na uniwersalne reprezentacje czasu przed wykonaniem obliczeń arytmetycznych daty. Aby zapoznać się z określoną listą operacji i odpowiednimi kontekstami strefy czasowej, zobacz tabelę w sekcji Sortowanie metod datetime.
  4. Obliczenie na wystąpieniu wartości DateTime nie modyfikuje wartości wystąpienia, dlatego wywołanie metody MyDateTime.ToLocalTime() nie modyfikuje wartości wystąpienia elementu DateTime. Metody skojarzone z klasami Date (w języku Visual Basic®) i DateTime (w środowisku .NET CLR) zwracają nowe wystąpienia reprezentujące wynik obliczenia lub operacji.
  5. W przypadku korzystania z .NET Framework w wersji 1.0 i 1.1 nie wysyłaj wartości Typu data/godzina reprezentująca czas UCT thru System.XML. Serializacja. Dotyczy to wartości daty, godziny i daty/godziny. W przypadku usług sieci Web i innych form serializacji do formatu XML obejmującego System.DateTime zawsze upewnij się, że wartość w wartości DateTime reprezentuje bieżący czas lokalny komputera. Serializator prawidłowo zdekoduje zdefiniowaną w schemacie XML wartość DateTime zakodowaną w gmt (wartość przesunięcia = 0), ale zdekoduje ją do punktu widzenia czasu komputera lokalnego.
  6. Ogólnie rzecz biorąc, jeśli masz do czynienia z bezwzględnym czasem, który upłynął, na przykład pomiarem limitu czasu, wykonywaniem arytmetyki lub porównywaniem różnych wartości datetime, należy spróbować użyć uniwersalnej wartości czasu, jeśli jest to możliwe, aby uzyskać najlepszą możliwą dokładność bez wpływu strefy czasowej i/lub oszczędzenia na światło dzienne.
  7. Podczas pracy z ogólnymi pojęciami, takimi jak planowanie, można bezpiecznie założyć, że każdy dzień ma 24 godziny z perspektywy użytkownika, może być w porządku, aby przeciwdziałać regule 6, wykonując arytmetyczne, et cetera, w czasie lokalnym.

W tym artykule ta prosta lista reguł stanowi podstawę zestawu najlepszych rozwiązań dotyczących pisania i testowania aplikacji, które przetwarzają daty.

Do tej pory kilku z was już przegląda twój kod i mówi: "Oh darn, to nie robi tego, czego się spodziewałem", co jest celem tego artykułu. Dla tych z nas, którzy nie mieli epifalii z czytania tej daleko, przyjrzyjmy się problemom związanym z przetwarzaniem wartości DateTime (od teraz po prostu skróć to do "dat") w . Aplikacje oparte na platformie NET.

Strategie magazynowania

Zgodnie z powyższymi regułami obliczenia dotyczące wartości daty są istotne tylko wtedy, gdy rozumiesz informacje o strefie czasowej skojarzone z przetwarzaną wartością daty. Oznacza to, że niezależnie od tego, czy przechowujesz wartość tymczasowo w zmiennej składowej klasy, czy też decydujesz się zapisać wartości zebrane w bazie danych lub pliku, jako programista jest odpowiedzialny za zastosowanie strategii, która umożliwia zrozumienie skojarzonych informacji o strefie czasowej w późniejszym czasie.

Najlepsze rozwiązanie nr 1

Podczas kodowania należy przechowywać informacje o strefie czasowej skojarzone z typem DateTime w zmiennej adjunct.

Alternatywą, ale mniej niezawodną, strategią jest utworzenie niezłomnej reguły, że przechowywane daty będą zawsze konwertowane na określoną strefę czasową, taką jak GMT, przed magazynem. Może to wydawać się rozsądne, a wiele zespołów może to zadziałać. Jednak brak jawnego sygnału, który mówi, że określona kolumna DateTime w tabeli w bazie danych znajduje się w określonej strefie czasowej niezmiennie prowadzi do błędów w interpretacji w kolejnych iteracji projektu.

Typowa strategia widziana w nieformalnym badaniu różnych elementów . Aplikacje oparte na platformie NET to chęć zawsze posiadania dat reprezentowanych w czasie uniwersalnym (GMT). Mówię "pragnienie", ponieważ nie zawsze jest to praktyczne. Przypadek w punkcie pojawia się podczas serializacji klasy, która ma zmienną składową DateTime za pośrednictwem usługi sieci Web. Przyczyną jest to, że typ wartości DateTime jest mapowany na typ XSD:DateTime (zgodnie z oczekiwaniami), a typ XSD uwzględnia punkty w czasie w dowolnej strefie czasowej. Omówimy później przypadek XML. Co ciekawe, dobry procent tych projektów nie osiągał ich celu i przechowywał informacje o dacie w strefie czasowej serwera bez ich realizacji.

W takich przypadkach interesującym faktem jest to, że testerzy nie widzieli problemów z konwersją czasu, więc nikt nie zauważył, że kod, który miał przekonwertować lokalne informacje o dacie na czas UCT, kończył się niepowodzeniem. W tych konkretnych przypadkach dane zostały później zserializowane za pośrednictwem kodu XML i zostały przekonwertowane prawidłowo, ponieważ informacje o dacie były w czasie lokalnym komputera na początek.

Przyjrzyjmy się kodowi , który nie działa:

Dim d As DateTime
d = DateTime.Parse("Dec 03, 2003 12:00:00 PM") 'date assignment
d.ToUniversalTime()

Powyższy program przyjmuje wartość w zmiennej d i zapisuje ją w bazie danych, oczekując, że przechowywana wartość będzie reprezentować widok czasu UCT. W tym przykładzie jest rozpoznawana, że metoda Analizy renderuje wynik w czasie lokalnym, chyba że niektóre kultury inne niż domyślne są używane jako opcjonalny argument dla rodziny metod Analizy.

Wcześniej pokazany kod rzeczywiście nie może przekonwertować wartości w zmiennej DateTime d na uniwersalny czas w trzecim wierszu, ponieważ zgodnie z zapisem przykład narusza regułę 4 (metody w klasie DateTime nie konwertują wartości bazowej). Uwaga: ten kod był widoczny w rzeczywistej aplikacji, która została przetestowana.

Jak to minęło? Zaangażowane aplikacje mogły pomyślnie porównać przechowywane daty, ponieważ podczas testowania wszystkie dane pochodziły z maszyn ustawionych na tę samą strefę czasową, więc reguła 1 została spełniona (wszystkie daty porównywane i obliczane są zlokalizowane w tym samym punkcie widoku strefy czasowej). Usterka w tym kodzie jest rodzajem, który jest trudny do wykrycia — instrukcja wykonywana, ale nie robi nic (wskazówka: ostatnia instrukcja w przykładzie jest operacją no-op, która została napisana).

Najlepsze rozwiązanie nr 2

Podczas testowania sprawdź, czy przechowywane wartości reprezentują wartość punktu w czasie, którą zamierzasz znaleźć w strefie czasowej, którą zamierzasz.

Naprawianie przykładu kodu jest łatwe:

Dim d As DateTime
d = DateTime.Parse("Dec 03, 2003 12:00:00 PM").ToUniversalTime()

Ponieważ metody obliczania skojarzone z typem wartości DateTime nigdy nie wpływają na wartość bazową, ale zamiast tego zwracają wynik obliczenia, program musi pamiętać o przechowywaniu przekonwertowanej wartości (jeśli jest to konieczne, oczywiście). Następnie sprawdzimy, jak nawet to pozornie właściwe obliczenie może nie osiągnąć oczekiwanych wyników w pewnych okolicznościach obejmujących czas letni.

Wykonywanie obliczeń

Na pierwszy rzut oka funkcje obliczeniowe, które są dostarczane z klasą System.DateTime, są naprawdę przydatne. Zapewniana jest obsługa dodawania interwałów do wartości czasu, wykonywania operacji arytmetycznych na wartościach czasu, a nawet konwertowania wartości czasu platformy .NET na odpowiedni typ wartości odpowiedni dla wywołań interfejsu API Win32®, a także wywołań automatyzacji OLE. Spojrzenie na metody pomocy technicznej, które otaczają typ DateTime przywołuje nostalgiczny wygląd wstecz na różne sposoby, które MS-DOS® i Windows® ewoluowały do radzenia sobie z czasem i znacznikami czasu na przestrzeni lat.

Fakt, że wszystkie te składniki są nadal obecne w różnych częściach systemu operacyjnego, są związane z wymaganiami dotyczącymi zgodności z poprzednimi wersjami, które firma Microsoft utrzymuje. Dla programisty oznacza to, że w przypadku przenoszenia danych reprezentujących znaczniki czasu na plikach, katalogach lub międzyoperacyjności COM/OLE obejmujących wartości daty i godziny, musisz być biegły w radzeniu sobie z konwersjami między różnymi generacjami czasu, które znajdują się w systemie Windows.

Nie daj się oszukać ponownie

Załóżmy, że przyjęto strategię "przechowujemy wszystko w czasie UCT", prawdopodobnie aby uniknąć narzutu związanego z koniecznością przechowywania przesunięcia strefy czasowej (i być może widoku strefy czasowej, na przykład czasu pacyficznego lub PST). Istnieje kilka zalet wykonywania obliczeń przy użyciu czasu UCT. Głównym z nich jest fakt, że gdy jest reprezentowany w uniwersalnym czasie, każdy dzień ma stałą długość i nie ma przesunięć strefy czasowej do czynienia z.

Jeśli byłeś zaskoczony czytanie, że dzień może mieć różne długości, należy pamiętać, że w każdej strefie czasowej, która pozwala na czas letni, w dwóch dniach roku (zazwyczaj), dni mają inną długość. Więc nawet jeśli używasz wartości czasu lokalnego, takiej jak Pacyfik (PST), jeśli spróbujesz dodać przedział czasu do określonej wartości wystąpienia DateTime, możesz nie uzyskać wyniku, który uważasz, że jeśli dodawany interwał trwa przeszłości zmiany w czasie w dniu rozpoczęcia lub zakończenia czasu letniego.

Przyjrzyjmy się przykładowi kodu, który nie działa w strefie czasowej Pacyfik w Stany Zjednoczone:

Dim d As DateTime
d = DateTime.Parse("Oct 26, 2003 12:00:00 AM") 'date assignment
d = d.AddHours(3.0)
' - displays 10/26/2003 03:00:00 AM – an ERROR!
MsgBox(d.ToString)

Wynik wyświetlany na podstawie tego obliczenia może wydawać się poprawny na pierwszy rzut oka; jednak 26 października 2003 r., jedna minuta po 1:59 CZASU PST, zmiana czasu letniego weszła w życie. Poprawna odpowiedź powinna wynosić 26.01.2003, 02:00:00, więc to obliczenie na podstawie wartości czasu lokalnego nie dało poprawnego wyniku. Ale jeśli spojrzymy wstecz na regułę nr 3, wydaje się, że mamy sprzeczność, ale nie. Nazwijmy ją specjalnym przypadkiem użycia metod dodawania/odejmowania w strefach czasowych, które świętują czas letni.

Najlepsze rozwiązanie nr 3

Podczas kodowania należy zachować ostrożność, jeśli musisz wykonać obliczenia daty/godziny (dodawanie/odejmowanie) na wartościach reprezentujących strefy czasowe, które praktykują czas letni. Mogą wynikać nieoczekiwane błędy obliczeń. Zamiast tego przekonwertuj wartość czasu lokalnego na uniwersalny czas, wykonaj obliczenia i przekonwertuj z powrotem, aby osiągnąć maksymalną dokładność.

Naprawienie uszkodzonego kodu jest proste:

Dim d As DateTime
d = DateTime.Parse("Oct 26, 2003 12:00:00 AM") 'date assignment
d = d.ToUniversalTime().AddHours(3.0).ToLocalTime()
' - displays 10/26/2003 02:00:00 AM – Correct!
MsgBox(d.ToString)
  

Najprostszym sposobem niezawodnego dodawania przedziałów czasu jest przekonwertowanie wartości opartych na czasie lokalnym na czas uniwersalny, wykonanie obliczeń, a następnie przekonwertowanie wartości z powrotem.

Sortowanie metod DateTime

W tym artykule omówiono różne metody klasy System.DateTime. Niektóre dają prawidłowy wynik, gdy wystąpienie bazowe reprezentuje czas lokalny, niektóre, gdy reprezentują uniwersalny czas, a inne nadal nie wymagają wystąpienia bazowego. Ponadto niektóre są całkowicie niezależne od strefy czasowej (np. AddYear, AddMonth). Aby uprościć ogólne zrozumienie założeń dotyczących najczęściej spotykanych metod obsługi typu DateTime, podano poniższą tabelę.

Aby odczytać tabelę, należy wziąć pod uwagę początkowy (wejściowy) i końcowy (zwrócony wartość) punkt widzenia. We wszystkich przypadkach stan końcowy wywoływania metody jest zwracany przez metodę . Nie dokonano konwersji na bazowe wystąpienie danych. Podano również zastrzeżenia opisujące wyjątki lub przydatne wskazówki.

Nazwa metody Punkt widoku początkowego Końcowy punkt widzenia Zastrzeżenia
Touniversaltime Czas lokalny UTC Nie należy wywoływać w wystąpieniu datetime, które już reprezentuje uniwersalny czas
Tolocaltime UTC Czas lokalny Nie należy wywoływać wystąpienia datetime, które już reprezentuje czas lokalny
ToFileTime Czas lokalny   Metoda zwraca metodę INT64 reprezentującą czas pliku Win32 (czas UCT)
FromFileTime   Czas lokalny Metoda statyczna — żadne wystąpienie nie jest wymagane. Przyjmuje czas INT64 UCT jako dane wejściowe
ToFileTimeUtc

(Tylko wersja 1.1)

UTC   Metoda zwraca metodę INT64 reprezentującą czas pliku Win32 (czas UCT)
FromFileTimeUtc

(Tylko wersja 1.1)

  UTC Metoda konwertuje czas pliku WIN32 INT64 na wystąpienie UCT daty/godziny
Now   Czas lokalny Metoda statyczna — żadne wystąpienie nie jest wymagane. Zwraca wartość typu DateTime reprezentującą bieżącą godzinę w czasie komputera lokalnego
Utcnow   UTC Metoda statyczna — żadne wystąpienie nie jest wymagane
Isleapyear Czas lokalny   Zwraca wartość logiczną, która wskazuje wartość true, jeśli część roku wystąpienia lokalnego czasu jest rokiem przestępnym.
Today   Czas lokalny Metoda statyczna — żadne wystąpienie nie jest wymagane. Zwraca wartość DateTime ustawioną na północ bieżącego dnia w czasie komputera lokalnego.

Specjalny przypadek XML

Kilka osób rozmawiałem niedawno miał cel projektowania serializacji wartości czasu w usługach sieci Web tak, że XML, który reprezentuje DateTime będzie sformatowany w GMT (np. z przesunięciem zerowym). Chociaż słyszałem różne powody, od chęci po prostu przeanalizowanie pola jako ciągu tekstowego do wyświetlenia w kliencie, aby zachować "przechowywane w UCT" założenia, które istnieją na serwerze do rozmówców usług sieci Web, nie byłem przekonany, że kiedykolwiek istnieje dobry powód, aby kontrolować format marshalling na przewodach do tego stopnia. Dlaczego? Ponieważ kodowanie XML typu DateTime jest idealnie odpowiednie do reprezentowania wystąpienia w czasie, a serializator XML wbudowany w .NET Framework wykonuje zadanie zarządzania serializacji i deserializacji problemów związanych z wartościami czasu.

Ponadto okazuje się, że wymuszanie System.XML. Serializator serializacji w celu zakodowania wartości daty w GMT na przewodach nie jest możliwe na platformie .NET, przynajmniej nie dzisiaj. Jako programista, projektant lub menedżer projektu zadanie staje się następnie upewniane, że dane przekazywane w aplikacji są wykonywane dokładnie z minimalnym kosztem.

Kilka grup, z którymi rozmawiałem w badaniach, które trafiły do tego artykułu, przyjęła strategię definiowania klas specjalnych i pisania własnych serializatorów XML, aby mieć pełną kontrolę nad tym, jak wartości DateTime w drutach wyglądały w ich XML. Chociaż podziwiam wyrywanie, które deweloperzy mają podczas skoku w tym odważnym przedsięwzięciu, upewnij się, że niuanse radzenia sobie z problemami z czasem dziennym i konwersją strefy czasowej sam powinien zrobić dobry menedżer powiedzieć: "Nie ma sposobu", zwłaszcza gdy mechanizmy przewidziane w .NET Framework zrobić doskonale dokładne zadanie serializacji wartości czasu już.

Istnieje tylko jedna sztuczka, o której musisz wiedzieć, a jako projektant musisz to zrozumieć i stosować się do reguły (zobacz Reguła nr 5).

Kod, który nie działa:

Najpierw zdefiniujmy prostą klasę XML ze zmienną składową DateTime. W celu ukończenia ta klasa jest uproszczonym odpowiednikiem zalecanego podejścia przedstawionego w dalszej części artykułu.

<XmlType(TypeName:="timeTestDef", _
    Namespace:= "http://tempuri.org/Timetester.xsd")>), _
    XmlRoot(), Serializable()> _
Public Class timeTestDef
    Private __timeVal As DateTime

    <XmlIgnore()> _
    Public timeValSpecified As Boolean

    <XmlElement(ElementName:="timeVal", IsNullable:=False, _
        Form:=XmlSchemaForm.Qualified, DataType:="dateTime", _
        Namespace:="http://tempuri.org/Timetester.xsd")> _
    Public Property timeVal() As DateTime
        Get
            timeVal = __timeVal
        End Get
        Set(ByVal Value As DateTime)
            __timeVal = Value
            timeValSpecified = True
        End Set
    End Property
End Class

Teraz użyjemy tej klasy do zapisania kodu XML do pliku.

' write out to the file
Dim t As Xml.XmlTextWriter
Dim ser As XmlSerializer
Dim tt As New timeTest ' a class that has a DateTime variable
' set the fields in your class
tt.timeVal = DateTime.Parse("12/12/2003 12:01:02 PM")
tt.timeVal = tt.TimeVal.ToUniversalTime()

' get a serializer for the root type, and serialize this UTC time
ser = New XmlSerializer(GetType(timeTest))
t = New Xml.XmlTextWriter("c:\timetest.xml", System.Text.Encoding.UTF8)
ser.Serialize(t, tt)
t.Close()
t = Nothing
tt = Nothing

Po uruchomieniu tego kodu kod XML, który jest serializowany do pliku wyjściowego, zawiera reprezentację XML DateTime w następujący sposób:

<timeVal>2003-12-12T20:01:02.0000000-08:00</timeVal> 

Jest to błąd: wartość zakodowana w kodzie XML jest wyłączona o osiem godzin! Ponieważ jest to przesunięcie strefy czasowej bieżącej maszyny, powinniśmy być podejrzani. Patrząc na sam kod XML, data jest prawidłowa, a data 20:01:02 odpowiada zegarowi w Londynie dla mojej własnej godziny w południe, ale część przesunięcia nie jest poprawna dla zegara z siedzibą w Londynie. Gdy kod XML wygląda jak czas w Londynie, przesunięcie powinno również reprezentować punkt widzenia w Londynie, którego ten kod nie osiąga.

Serializator XML zawsze zakłada, że wartości DateTime są serializowane reprezentują czas komputera lokalnego, więc stosuje przesunięcie lokalnej strefy czasowej maszyny jako część przesunięcia zakodowanego czasu XML. Gdy deserializujemy to na innej maszynie, oryginalne przesunięcie jest odejmowane od wartości analizowanej, a przesunięcie strefy czasowej bieżącej maszyny jest dodawane.

Gdy zaczynamy od czasu lokalnego, wynik serializacji (kodowanie do daty/godziny XML, a następnie dekodowanie do czasu komputera lokalnego) jest zawsze poprawne — ale tylko wtedy, gdy początkowa wartość DateTime jest serializowana reprezentuje czas lokalny po rozpoczęciu serializacji. W przypadku tego uszkodzonego przykładu kodu wartość DateTime została już dostosowana w zmiennej składowej timeVal do czasu UCT, więc po serializacji i deserializacji wynik jest wyłączony przez liczbę godzin równych przesunięciu strefy czasowej maszyny źródłowej. To jest złe.

Najlepsze rozwiązanie nr 4

Podczas testowania oblicz oczekiwaną wartość w ciągu XML, który jest serializowany przy użyciu lokalnego widoku czasu komputera testowanego punktu w czasie. Jeśli kod XML w strumieniu serializacji będzie się różnić, zarejestruj usterkę!

Naprawianie tego kodu jest proste. Oznacz jako komentarz wiersz wywołujący funkcję ToUniversalTime().

Najlepsze rozwiązanie nr 5

Podczas pisania kodu w celu serializacji klas, które mają zmienne składowe DateTime, wartości muszą reprezentować czas lokalny. Jeśli nie zawierają czasu lokalnego, dostosuj je przed jakimkolwiek krokiem serializacji, w tym przekazywaniem lub zwracaniem typów zawierających wartości DateTime w usługach sieci Web.

Klasa Coders Quandary

Wcześniej przyjrzeliśmy się dość nieofilistycznej klasie, która uwidoczniła właściwość DateTime. W tej klasie po prostu serializowaliśmy to, co przechowywaliśmy w klasie DateTime, bez względu na to, czy wartość reprezentuje lokalny lub uniwersalny punkt widzenia czasu. Przyjrzyjmy się bardziej zaawansowanemu podejściu, które oferuje programistom jawny wybór co do tego, jakie założenia strefy czasowej chcą, podczas gdy zawsze serializują prawidłowo.

Podczas kodowania klasy, która będzie mieć zmienną składową typu DateTime, programista ma możliwość upublicznienia zmiennej składowej lub zapisania logiki właściwości w celu opakowania zmiennej składowej za pomocą operacji get/set . Wybranie typu publicznego ma kilka wad, które w przypadku typów DateTime mogą mieć konsekwencje, które nie są objęte kontrolą dewelopera klasy.

Korzystając z poznanych do tej pory informacji, rozważ użycie dwóch właściwości dla każdego typu DateTime.

Poniższy przykład ilustruje zalecane podejście do zarządzania zmiennymi składowymi DateTime:

<XmlType(TypeName:="timeTestDef", _
    Namespace:= "http://tempuri.org/Timetester.xsd")>), _
    XmlRoot(), Serializable(), _
    EditorBrowsable(EditorBrowsableState.Advanced)> _
Public Class timeTestDef
    Private __timeVal As DateTime

    <XmlIgnore()> _
    Public timeValSpecified As Boolean

    <XmlElement(ElementName:="timeVal", IsNullable:=False, _
        Form:=XmlSchemaForm.Qualified, DataType:="dateTime", _
        Namespace:="http://tempuri.org/Timetester.xsd")> _
    Public Property timeVal() As DateTime
        Get
            timeVal = __timeVal.ToLocalTime()
        End Get
        Set(ByVal Value As DateTime)
            __timeVal = Value.ToUniversalTime()
            timeValSpecified = True
        End Set
    End Property

    <XmlIgnore()> _
    Public Property timeValUTC() As DateTime
        Get
            timeValUTC = __timeVal
        End Get
        Set(ByVal Value As DateTime)
            __timeVal = Value
            timeValSpecified = True
        End Set
    End Property
End Class

Ten przykład jest poprawionym odpowiednikiem poprzedniego przykładu serializacji klasy. W obu przykładach klas (ten i wcześniejszy) klasy są implementacjami opisanymi przy użyciu następującego schematu:

<?xml version="1.0" encoding="utf-8" ?> 
<xs:schema id="Timetester" 
     targetNamespace="http://tempuri.org/Timetester.xsd"
     elementFormDefault="qualified"
     xmlns="http://tempuri.org/Timetester.xsd"
     xmlns:mstns="http://tempuri.org/Timetester.xsd"
     xmlns:xs="http://www.w3.org/2001/XMLSchema">

     <xs:element name="timeTest" type="timeTestDef"/>
     <xs:complexType name="timeTestDef">
      <xs:sequence>
         <xs:element name="timeVal" type="xs:dateTime"/>
      </xs:sequence>
     </xs:complexType>
</xs:schema>

W tym schemacie i we wszystkich implementacjach klas definiujemy zmienną składową, która reprezentuje opcjonalną wartość czasu. W naszym zalecanym przykładzie udostępniliśmy dwie właściwości zarówno metod pobierających, jak i ustawiających — jedną dla uniwersalnego czasu i jedną dla czasu lokalnego. Atrybuty w nawiasach kątowych widoczne w kodzie informują serializator XML o użyciu wersji czasu lokalnego na potrzeby serializacji i zazwyczaj sprawiają, że implementacja klasy powoduje wynik w danych wyjściowych zgodnych ze schematem. Aby klasa prawidłowo poradziła sobie z opcjonalnym brakiem wyrażenia, gdy w wystąpieniu nie ustawiono żadnej wartości, zmienna timeValSpecified i skojarzona logika w ustawieniu właściwości kontroluje, czy element XML jest wyrażony w czasie serializacji, czy nie. To opcjonalne zachowanie wykorzystuje funkcję w podsystemie serializacji, która została zaprojektowana do obsługi opcjonalnej zawartości XML.

To podejście do zarządzania wartościami DateTime w klasach platformy .NET zapewnia najlepsze wyniki obu światów — uzyskujesz dostęp do magazynu na podstawie uniwersalnego czasu, dzięki czemu obliczenia są dokładne i uzyskujesz właściwą serializacji widoków czasu lokalnego.

Najlepsze rozwiązanie nr 6

Podczas kodowania ustaw zmienne składowe DateTime jako prywatne i podaj dwie właściwości do manipulowania elementami członkowskimi DateTime w czasie lokalnym lub uniwersalnym. Stronnicza odchylenie magazynu w prywatnym elemencie członkowskim jako czas UCT przez kontrolowanie logiki w elementach pobierających i ustawiających. Dodaj atrybuty serializacji XML do deklaracji właściwości czasu lokalnego, aby upewnić się, że wartość czasu lokalnego jest serializowana (zobacz przykład).

Zastrzeżenia do tego podejścia

Zalecane podejście do zarządzania datą/godziną w uniwersalnej godzinie w prywatnych zmiennych członkowskich jest prawidłowe, podobnie jak zalecenie udostępnienia podwójnych właściwości, aby umożliwić koderom radzenie sobie z wersjami czasu, z którymi są najbardziej wygodne. Jednym z problemów, które deweloper korzystający z tej lub innej metody, która uwidacznia dowolny czas lokalny w programie, nadal jest problemem 25-godzinnym w czasie dziennym. Będzie to nadal problem w przypadku programów korzystających ze środowiska CLR w wersji 1.0 i 1.1, dlatego należy pamiętać, czy program wchodzi w ten specjalny przypadek (dodawana lub brakująca godzina na reprezentowany czas) i dostosuj ręcznie. Dla tych, którzy nie mogą tolerować jednogodzinnego okna problemu rocznie, bieżące zalecenie polega na przechowywaniu dat jako ciągów lub innej metody samodzielnej zarządzania. (Długie liczby całkowite w systemie Unix są dobrym rozwiązaniem).

W przypadku środowiska CLR w wersji 2.0 (dostępnej w nadchodzącej wersji programu Visual Studio® o nazwie "Whidbey"), świadomość, czy data/godzina zawiera czas lokalny, czy uniwersalna wartość czasu jest dodawana do .NET Framework. W tym momencie zalecany wzorzec będzie nadal działać, ale w przypadku programów, które wchodzą w interakcję ze zmiennymi składowymi za pośrednictwem właściwości UTC, te błędy w brakującym/dodatkowym okresie godzinowym zostaną wyeliminowane. Z tego powodu najlepszym rozwiązaniem w zakresie kodowania przy użyciu dwóch właściwości jest obecnie zdecydowanie sugerowane, dzięki czemu programy będą migrować czystą migrację do środowiska CLR w wersji 2.0.

Obsługa czasu letniego

Gdy przygotowujemy się do zamknięcia i opuszczenia tematu kodowania i testowania praktyk dotyczących wartości DateTime, pozostaje jeden szczególny przypadek, który należy zrozumieć. Ten przypadek obejmuje niejednoznaczności, które otaczają czas letni i powtarzany jeden godzinę na rok problem. Ten problem dotyczy głównie aplikacji, które zbierają wartości czasu z danych wejściowych użytkownika.

Dla tych z was w większości z liczyć kraju, ten przypadek jest trywialny, ponieważ w większości krajów letnich czas letni nie jest praktykowany. Ale dla tych z was, którzy są w dotkniętych większości programów (czyli wszystkich, którzy mają aplikacje, które muszą zajmować się czasem, które mogą być reprezentowane w miejscach, w których PRAKTYKują letnie oszczędności), musisz wiedzieć, że ten problem istnieje i uwzględnić go.

W obszarach świata, które praktykują czas letni, jest jedna godzina każdej jesieni i wiosny, gdzie czas pozornie idzie siana. W nocy, że czas zegara zmienia się z czasu standardowego na czas letni, czas skoki przed godziną. Dzieje się to wiosną. Jesienią roku, w jedną noc, zegar czasu lokalnego skacze z powrotem godzinę.

W tych dniach można napotkać warunki, w których długość dnia wynosi 23 lub 25 godzin. Dlatego w przypadku dodawania lub odejmowania przedziałów czasu z wartości daty i zakresu przekracza ten dziwny punkt w czasie, w którym zegary przełączają się, kod musi dokonać ręcznego dostosowania.

W przypadku logiki używającej metody DateTime.Parse() do obliczenia wartości DateTime na podstawie danych wejściowych użytkownika określonej daty i godziny należy wykryć, że niektóre wartości są nieprawidłowe (w ciągu 23-godzinnego dnia), a niektóre wartości mają dwa znaczenie, ponieważ określona godzina powtarza się (w ciągu 25-godzinnego dnia). Aby to zrobić, musisz znać zaangażowane daty i szukać tych godzin. Może być przydatne analizowanie i ponowne redystruowanie interpretowanych informacji o dacie, gdy użytkownik zamyka pola używane do wprowadzania dat. Z reguły należy unikać określania przez użytkowników czasu letniego w danych wejściowych.

Omówiliśmy już najlepsze rozwiązanie w zakresie obliczeń obejmujących czas. Konwertując widoki czasu lokalnego na uniwersalny czas przed wykonaniem obliczeń, przechodzisz poza problemy z dokładnością czasu. Trudniejszą do zarządzania sprawą jest przypadek niejednoznaczności związany z analizowaniem danych wejściowych użytkownika, które występują w tej magicznej godzinie wiosną i jesienią.

Obecnie nie ma możliwości analizowania ciągu reprezentującego widok czasu użytkownika i dokładnego przypisania uniwersalnej wartości czasu. Powodem jest to, że ludzie, którzy doświadczają czasu letniego, nie mieszkają w miejscach, w których strefa czasowa to Greenwich Mean Time. W związku z tym jest całkowicie możliwe, że ktoś mieszkający na wschodnim wybrzeżu Stany Zjednoczone wpisze wartość jak "26 października 2003 01:10:00 am".

W tym konkretnym ranku, o godzinie 2:00, zegar lokalny jest resetowany do godziny 1:00, tworząc 25-godzinny dzień. Ponieważ wszystkie wartości czasu zegara z zakresu od 1:00 do 2:00 występują dwa razy w danym dniu rano — przynajmniej w większości Stanów Zjednoczonych i Kanady. Komputer naprawdę nie ma możliwości, aby dowiedzieć się, która godzina 1:10 miała na celu — ten, który występuje przed przełączenie, lub ten, który występuje 10 minut po przełączeniu czasu letniego.

Podobnie programy muszą radzić sobie z problemem, który występuje wiosną, kiedy w określonym ranku nie ma takiej godziny jak 2:10. Powodem jest to, że o 2:00 w tym konkretnym ranku, godzina na zegarach lokalnych nagle zmienia się na 3:00. Cała godzina 2:00 nigdy się nie dzieje w tym 23-godzinnym dniu.

Programy muszą radzić sobie z tymi przypadkami, prawdopodobnie przez monitowanie użytkownika o wykrycie niejednoznaczności. Jeśli nie zbierasz ciągów daty i godziny od użytkowników i analizujesz je, prawdopodobnie nie masz tych problemów. Programy, które muszą określić, czy określony czas przypada w czasie dziennym, może korzystać z następujących elementów:

Timezone.CurrentTimeZone.IsDaylightSavingTime(DateTimeInstance)

lub

DateTimeInstance.IsDaylightSavingTime

Najlepsze rozwiązanie nr 7

Podczas testowania, jeśli programy akceptują dane wejściowe użytkownika określające wartości daty i godziny, pamiętaj, aby przetestować utratę danych na "spring-ahead", "fall-back" 23- i 25-godzinne dni. Pamiętaj również, aby przetestować daty zebrane na maszynie w jednej strefie czasowej i przechowywane na maszynie w innej strefie czasowej.

Formatowanie i analizowanie wartości User-Ready

W przypadku programów, które pobierają informacje o dacie i godzinie od użytkowników i muszą konwertować te dane wejściowe użytkownika na wartości Typu data/godzina, platforma zapewnia obsługę analizowania ciągów sformatowanych w określony sposób. Ogólnie rzecz biorąc, metody DateTime.Parse i ParseExact są przydatne do konwertowania ciągów zawierających daty i godziny na wartości DateTime. Z drugiej strony metody ToString, ToLongDateString, ToLongTimeString, ToShortDateString i ToShortTimeString są przydatne do renderowania wartości DateTime na ciągi czytelne dla człowieka.

Dwa główne problemy, które mają wpływ na analizowanie, to kultura i ciąg formatu. W artykule DateTime Frequently Asked Questions (FAQ) opisano podstawowe problemy związane z kulturą, więc w tym miejscu skoncentrujemy się na najlepszych rozwiązaniach dotyczących ciągu formatu, które mają wpływ na analizowanie daty/godziny.

Zalecane ciągi formatu do konwertowania daty/godziny na ciągi to:

'yy'-'MM'-'DD'T'HH': 'mm': 'ss.fffffff'Z' — dla wartości UCT

'yy'-'MM'-'dd'T'HH': 'mm': 'ss.fffffff'zzz' — dla wartości lokalnych

'yyyy'-'MM'-'dd'T'HH': 'mm': 'ss.fffffff' — dla abstrakcyjnych wartości czasu

Są to wartości ciągu formatu, które zostaną przekazane do metody DateTime.ToString , jeśli chcesz uzyskać dane wyjściowe zgodne ze specyfikacją typu DateTime XML. Cytaty zapewniają, że lokalne ustawienia daty i godziny na maszynie nie zastępują opcji formatowania. Jeśli musisz określić różne układy, możesz przekazać inne ciągi formatu dla dość elastycznej funkcji renderowania dat, ale należy zachować ostrożność, aby używać tylko notacji Z do renderowania ciągów z wartości UCT i używać notacji zzz dla wartości czasu lokalnego.

Analizowanie ciągów i konwertowanie ich na wartości DateTime można wykonać za pomocą metod DateTime.Parse i ParseExact. Dla większości z nas analizowanie jest wystarczające, ponieważ funkcja ParseExact wymaga podania własnego wystąpienia obiektu Formatter . Analizowanie jest bardzo zdolne i elastyczne i może dokładnie konwertować większość ciągów zawierających daty i godziny.

Na koniec ważne jest, aby zawsze wywoływać metody Parse i ToString dopiero po ustawieniu właściwości CultureInfo wątku na CultureInfo.InvariantCulture.

Przyszłe zagadnienia

Jedną z rzeczy, których nie można łatwo wykonać w przypadku elementu DateTime.ToString, jest formatowanie wartości DateTime w dowolnej strefie czasowej. Ta funkcja jest rozważana w przyszłych implementacjach .NET Framework. Jeśli musisz mieć możliwość określenia, że ciąg "12:00:00 EST" jest odpowiednikiem ciągu "11:00:00 EDT", musisz samodzielnie obsłużyć konwersję i porównanie.

Problemy z metodą DateTime.Now()

Istnieje kilka problemów podczas pracy z metodą o nazwie Now. W przypadku deweloperów języka Visual Basic czytających tę funkcję dotyczy również funkcji Visual Basic Now . Deweloperzy, którzy regularnie korzystają z metody Now, wiedzą, że jest ona często używana do uzyskiwania bieżącej godziny. Wartość zwrócona przez metodę Now znajduje się w bieżącym kontekście strefy czasowej maszyny i nie może być traktowana jako wartość niezmienna. Powszechną praktyką jest konwertowanie czasów, które mają być przechowywane lub wysyłane między maszynami do czasu uniwersalnego (UCT).

Jeśli czas letni jest możliwy, istnieje jedna praktyka kodowania, której należy unikać. Rozważmy następujący kod, który może wprowadzić trudną do wykrycia usterkę:

Dim timeval As DateTime
timeval = DateTime.Now().ToUniversalTime()  

Wartość, która będzie wynikać z uruchomienia tego kodu, zostanie wyłączona o godzinę, jeśli zostanie wywołana w ciągu dodatkowej godziny, która występuje podczas przełączania czasu letniego jesienią. (Dotyczy to tylko maszyn, które znajdują się w strefach czasowych, które praktykują czas letni). Ponieważ dodatkowa godzina przypada w tym miejscu, gdzie ta sama wartość, taka jak 1:10:00, występuje dwa razy rano, zwrócona wartość może nie odpowiadać żądanej wartości.

Aby rozwiązać ten problem, najlepszym rozwiązaniem jest wywołanie metody DateTime.UtcNow() zamiast wywołania metody DateTime.Now, a następnie przekonwertowanie na uniwersalny czas.

Dim timeval As DateTime
timeval = DateTime.UtcNow()  

Ten kod zawsze będzie miał właściwą perspektywę 24-godzinną i może zostać bezpiecznie przekonwertowany na czas lokalny.

Najlepsze rozwiązanie nr 8

Gdy kodujesz i chcesz przechowywać bieżący czas reprezentowany jako uniwersalny czas, unikaj wywoływania metody DateTime.Now(), a następnie konwersji na czas uniwersalny. Zamiast tego wywołaj funkcję DateTime.UtcNow bezpośrednio.

Zastrzeżenie: jeśli zamierzasz serializować klasę zawierającą wartość DateTime, upewnij się, że wartość serializowana nie reprezentuje czasu uniwersalnego. Serializacja XML nie będzie obsługiwać serializacji UCT do momentu wydania Whidbey programu Visual Studio.

Kilka mało znanych dodatków

Czasami, gdy zaczynasz zagłębiać się w część interfejsu API, znajdujesz ukryty klejnot — coś, co pomaga osiągnąć cel, ale które, jeśli o tym nie mówisz, nie odkrywasz w codziennych podróżach. Typ wartości DateTime na platformie .NET ma kilka takich klejnotów, które mogą pomóc w osiągnięciu bardziej spójnego wykorzystania uniwersalnego czasu.

Pierwszy to wyliczenie DateTimeStyles , które znajduje się w przestrzeni nazw System.Globalization . Wyliczenie steruje zachowaniami funkcji DateTime.Parse() i ParseExact, które są używane do konwertowania danych wejściowych określonych przez użytkownika i innych form reprezentacji ciągu wejściowego na wartości DateTime.

W poniższej tabeli przedstawiono niektóre funkcje włączone przez wyliczenie DateTimeStyles.

Wyliczenie, stała Przeznaczenie Zastrzeżenia
Dostosowywanie elementu Douniversal Po przekazaniu jako części metody Parse lub ParseExact ta flaga powoduje, że zwracana wartość jest uniwersalna. Dokumentacja jest niejednoznaczna, ale działa zarówno z parse, jak i parseExact.
NoCurrentDateDefault Pomija założenie, że ciągi analizowane bez składników daty będą miały zwróconą wartość DateTime, która jest godziną bieżącej daty. Jeśli ta opcja jest używana, zwracana wartość DateTime to godzina określona w dacie gregoriańskiej 1 stycznia w roku 1.
AllowWhiteSpaces

AllowTrailingWhite

AllowLeadingWhite

AllowInnerWhite

Te opcje umożliwiają tolerancję dla dodanych białych spacji przed, za i w środku analizowanych ciągów dat. Brak

Inne interesujące funkcje obsługi znajdują się w klasie System.Timezone . Pamiętaj, aby sprawdzić te informacje, jeśli chcesz wykryć, czy czas letni będzie mieć wpływ na wartość DateTime, czy też chcesz programowo określić bieżące przesunięcie strefy czasowej dla komputera lokalnego.

Podsumowanie

Klasa .NET Framework DateTime udostępnia w pełni funkcjonalny interfejs do pisania programów, które zajmują się czasem. Zrozumienie niuansów radzenia sobie z klasą wykracza poza to, co można uzyskać z funkcji IntelliSense®. W tym miejscu omówiliśmy najlepsze rozwiązania dotyczące kodowania i testowania programów, które zajmują się datami i godzinami. Szczęśliwe kodowanie!