Partager via


Meilleures pratiques de codage à l’aide de DateTime dans le .NET Framework

 

Dan Rogers
Microsoft Corporation

Février 2004

S’applique à
   Microsoft® .NET Framework
   ® Microsoft ASP.NET Web Services
   sérialisation XML

Résumé: L’écriture de programmes qui stockent, effectuent des calculs et sérialisent des valeurs d’heure à l’aide du type DateTime dans Microsoft .NET Framework nécessite une prise de conscience des différents problèmes associés aux représentations temporelles disponibles dans Windows et .NET. Cet article se concentre sur les principaux scénarios de test et de développement impliquant du temps et définit les recommandations de bonnes pratiques pour l’écriture de programmes qui utilisent le type DateTime dans Microsoft . Applications et assemblys basés sur NET. (18 pages imprimées)

Contenu

Contexte
   Qu’est-ce qu’un DateTime, de toute façon ?
   Les règles
Stratégies de stockage
   Meilleure pratique n° 1
   Bonne pratique n° 2
Exécution de calculs
   Ne vous trompez pas encore
   Meilleure pratique n° 3
   Tri des méthodes DateTime
Cas particulier de XML
   Meilleure pratique n° 4
   Meilleure pratique n° 5
Le problème des codeurs de classe
   Meilleure pratique n° 6
Gestion de l’heure d’été
   Meilleure pratique n° 7
Mise en forme et analyse des valeurs User-Ready
   Considérations futures
Problèmes liés à la méthode DateTime.Now()
   Meilleure pratique n° 8
Quelques extras peu connus
Conclusion

Contexte

De nombreux programmeurs rencontrent des affectations qui les obligent à stocker et à traiter avec précision les données qui contiennent des informations de date et d’heure. À première vue, le type de données DateTime du Common Language Runtime (CLR) semble parfait pour ces tâches. Toutefois, il n’est pas rare que les programmeurs, mais plus probablement les testeurs, rencontrent des cas où un programme perd simplement la trace des valeurs de temps correctes. Cet article se concentre sur les problèmes associés à la logique impliquant DateTime et, ce faisant, présente les meilleures pratiques en matière d’écriture et de test de programmes qui capturent, stockent, récupèrent et transmettent des informations DateTime.

Qu’est-ce qu’un DateTime, de toute façon ?

Lorsque nous examinons la documentation de la bibliothèque de classes NET Framework, nous constatons que « Le type de valeur CLR System.DateTime représente des dates et des heures allant de 12:00:00 minuit, 1er janvier 0001 AD à 23:59:59, décembre 31 9999 AD ». En lisant plus loin, nous apprenons, sans surprise, qu’une valeur DateTime représente un instant à un point dans le temps, et qu’une pratique courante consiste à enregistrer des valeurs à un point dans le temps universel coordonné (UCT), plus communément appelé Heure moyenne de Greenwich (GMT).

À première vue, un programmeur découvre alors qu’un type DateTime est assez bon pour stocker des valeurs d’heure susceptibles d’être rencontrées dans les problèmes de programmation actuels, tels que dans les applications métier. Avec cette confiance, de nombreux programmeurs peu méfiants commencent à coder, confiants qu’ils peuvent apprendre autant qu’ils en ont besoin à peu près le temps qu’ils avancent. Cette approche « learn-as-you-go » peut vous conduire à quelques problèmes. Nous allons donc commencer à les identifier. Ils vont des problèmes de la documentation aux comportements qui doivent être pris en compte dans la conception de votre programme.

La documentation V1.0 et 1.1 pour System.DateTime effectue quelques généralisations qui peuvent mettre hors piste le programmeur peu méfiant. Pour instance, la documentation indique toujours que les méthodes et propriétés trouvées dans la classe DateTime utilisent toujours l’hypothèse que la valeur représente le fuseau horaire local de l’ordinateur local lors de calculs ou de comparaisons. Cette généralisation s’avère fausse, car il existe certains types de calculs de date et d’heure qui supposent l’heure GMT, et d’autres qui supposent un affichage de fuseau horaire local. Ces domaines sont signalés plus loin dans cet article.

Nous allons donc commencer par explorer le type DateTime en décrivant une série de règles et de bonnes pratiques qui peuvent vous aider à faire fonctionner votre code correctement la première fois.

Les règles

  1. Les calculs et les comparaisons des instances DateTime ne sont significatifs que lorsque les instances comparées ou utilisées sont des représentations de points dans le temps du même point de vue de fuseau horaire.
  2. Un développeur est responsable du suivi des informations de fuseau horaire associées à une valeur DateTime via un mécanisme externe. En règle générale, cela s’effectue en définissant un autre champ ou une autre variable que vous utilisez pour enregistrer les informations de fuseau horaire lorsque vous stockez un type de valeur DateTime. Cette approche (qui stocke l’sens du fuseau horaire avec la valeur DateTime) est la plus précise et permet à différents développeurs à différents moments du cycle de vie d’un programme de toujours avoir une compréhension claire de la signification d’une valeur DateTime. Une autre approche courante consiste à faire de votre conception une « règle » selon laquelle toutes les valeurs temporelles sont stockées dans un contexte de fuseau horaire spécifique. Cette approche ne nécessite pas de stockage supplémentaire pour enregistrer la vue d’un utilisateur du contexte de fuseau horaire, mais elle introduit le risque qu’une valeur d’heure soit mal interprétée ou stockée de manière incorrecte par un développeur qui ne connaît pas la règle.
  3. L’exécution de calculs de date et d’heure sur des valeurs qui représentent l’heure locale de l’ordinateur peut ne pas toujours produire le résultat correct. Lorsque vous effectuez des calculs sur des valeurs de temps dans des contextes de fuseau horaire qui pratiquent l’heure d’été, vous devez convertir des valeurs en représentations d’heure universelles avant d’effectuer des calculs arithmétiques de date. Pour obtenir la liste spécifique des opérations et les contextes de fuseau horaire appropriés, consultez le tableau de la section Tri des méthodes DateTime.
  4. Un calcul sur une instance d’une valeur DateTime ne modifie pas la valeur de l’instance. Par conséquent, un appel à MyDateTime.ToLocalTime() ne modifie pas la valeur de l’instance du DateTime. Les méthodes associées aux classes Date (en Visual Basic®) et DateTime (dans le CLR .NET) retournent de nouvelles instances qui représentent le résultat d’un calcul ou d’une opération.
  5. Lorsque vous utilisez .NET Framework version 1.0 et 1.1, N’envoyez PAS de valeur DateTime qui représente l’heure UCT par System.XML. Sérialisation. Cela vaut pour les valeurs Date, Time et DateTime. Pour les services Web et d’autres formes de sérialisation au format XML impliquant System.DateTime, assurez-vous toujours que la valeur dans la valeur DateTime représente l’heure locale de l’ordinateur actuel. Le sérialiseur décode correctement une valeur DateTime définie par le schéma XML qui est encodée en GMT (valeur de décalage = 0), mais il la décodera en mode heure locale de l’ordinateur.
  6. En général, si vous avez affaire à un temps écoulé absolu, comme la mesure d’un délai d’expiration, l’exécution d’arithmétiques ou la comparaison de différentes valeurs DateTime, vous devez essayer d’utiliser une valeur de temps universelle si possible afin d’obtenir la meilleure précision possible sans que les effets des économies de fuseau horaire et/ou d’été n’ont un impact.
  7. Lorsque vous traitez des concepts de haut niveau, orientés utilisateur tels que la planification, et que vous pouvez supposer en toute sécurité que chaque jour a 24 heures du point de vue d’un utilisateur, il peut être possible de contrer la règle n° 6 en effectuant des arithmétiques, et cetera, aux heures locales.

Tout au long de cet article, cette liste simple de règles sert de base à un ensemble de bonnes pratiques pour l’écriture et le test d’applications qui traitent des dates.

À l’heure actuelle, plusieurs d’entre vous examinent déjà votre code et disent : « Oh darn, il ne fait pas ce que je m’attendais à ce qu’il fasse », ce qui est l’objectif de cet article. Pour ceux d’entre nous qui n’ont pas eu d’épiphanie de lecture jusqu’ici, examinons les problèmes associés au traitement des valeurs DateTime (à partir de maintenant, je vais simplement raccourcir cela à « dates ») dans . Applications basées sur NET.

Stratégies de stockage

Selon les règles ci-dessus, les calculs sur les valeurs de date n’ont de sens que si vous comprenez les informations de fuseau horaire associées à la valeur de date que vous traitez. Cela signifie que, que vous stockiez temporairement votre valeur dans une variable membre de classe ou que vous choisissiez d’enregistrer les valeurs que vous avez collectées dans une base de données ou un fichier, vous êtes responsable, en tant que programmeur, d’appliquer une stratégie qui permet aux informations de fuseau horaire associées d’être comprises ultérieurement.

Meilleure pratique n° 1

Lors du codage, stockez les informations de fuseau horaire associées à un type DateTime dans une variable complémentaire.

Une autre stratégie, mais moins fiable, consiste à faire en sorte que vos dates stockées soient toujours converties en un fuseau horaire particulier, tel que GMT, avant le stockage. Cela peut sembler raisonnable, et de nombreuses équipes peuvent le faire fonctionner. Toutefois, l’absence d’un signal d’affichage indiquant qu’une colonne DateTime particulière dans une table d’une base de données se trouve dans un fuseau horaire spécifique entraîne invariablement des erreurs d’interprétation dans les itérations ultérieures d’un projet.

Stratégie courante observée dans une enquête informelle de différents . Les applications basées sur NET sont le désir de toujours avoir des dates représentées en heure universelle (GMT). Je dis « désir » parce que ce n’est pas toujours pratique. Un cas se produit lors de la sérialisation d’une classe qui a une variable membre DateTime via un service Web. La raison en est qu’un type valeur DateTime est mappé à un type XSD:DateTime (comme on peut s’y attendre), et que le type XSD prend en charge la représentation des points dans le temps dans n’importe quel fuseau horaire. Nous aborderons le cas XML plus tard. Plus intéressant encore, un bon pourcentage de ces projets n’ont pas atteint leur objectif et stockaient les informations de date dans le fuseau horaire du serveur sans s’en rendre compte.

Dans ces cas, un fait intéressant est que les testeurs ne voyaient pas de problèmes de conversion d’heure, donc personne n’avait remarqué que le code qui était censé convertir les informations de date locale en heure UCT a échoué. Dans ces cas spécifiques, les données ont ensuite été sérialisées via XML et converties correctement, car les informations de date étaient à l’heure locale de l’ordinateur pour commencer.

Examinons un code qui ne fonctionne pas :

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

Le programme ci-dessus prend la valeur dans la variable d et l’enregistre dans une base de données, en s’attendant à ce que la valeur stockée représente une vue UCT du temps. Cet exemple reconnaît que la méthode Parse restitue le résultat en heure locale, sauf si une culture autre que celle par défaut est utilisée comme argument facultatif pour la famille de méthodes Parse.

Le code affiché précédemment ne parvient pas à convertir la valeur de la variable DateTime d en heure universelle sur la troisième ligne, car, tel qu’il est écrit, l’exemple enfreint la règle n° 4 (les méthodes de la classe DateTime ne convertissent pas la valeur sous-jacente). Remarque : ce code a été vu dans une application réelle qui avait été testée.

Comment est-il passé ? Les applications impliquées ont pu comparer correctement les dates stockées, car, pendant le test, toutes les données provenaient de machines définies sur le même fuseau horaire, de sorte que la règle n° 1 a été satisfaite (toutes les dates comparées et calculées sont localisées sur le même point de vue de fuseau horaire). Le bogue dans ce code est du type difficile à repérer: une instruction qui s’exécute mais qui ne fait rien (conseil : la dernière instruction de l’exemple est une instruction sans opération telle qu’elle est écrite).

Bonne pratique n° 2

Lors du test, case activée de voir que les valeurs stockées représentent la valeur à un instant dans le temps que vous souhaitez dans le fuseau horaire que vous souhaitez.

Il est facile de corriger l’exemple de code :

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

Étant donné que les méthodes de calcul associées au type de valeur DateTime n’ont jamais d’impact sur la valeur sous-jacente, mais retournent plutôt le résultat du calcul, un programme doit se rappeler de stocker la valeur convertie (si cela est souhaité, bien sûr). Nous allons ensuite examiner comment même ce calcul apparemment correct peut ne pas atteindre les résultats attendus dans certaines circonstances impliquant l’heure d’été.

Exécution de calculs

À première vue, les fonctions de calcul fournies avec la classe System.DateTime sont vraiment utiles. La prise en charge est fournie pour l’ajout d’intervalles aux valeurs d’heure, l’exécution de valeurs arithmétiques sur le temps et même la conversion de valeurs d’heure .NET en type valeur correspondant approprié pour les appels d’API Win32®, ainsi que les appels OLE Automation. Un regard sur les méthodes de support qui entourent le type DateTime évoque un regard nostalgique sur les différentes façons dont MS-DOS® et Windows® ont évolué pour gérer le temps et les horodatages au fil des ans.

Le fait que tous ces composants soient toujours présents dans différentes parties du système d’exploitation est lié aux exigences de compatibilité descendante que Microsoft gère. Pour un programmeur, cela signifie que si vous déplacez des données représentant des horodatages sur des fichiers, des répertoires ou effectuez COM/OLE Interop impliquant des valeurs date et DateTime, vous devez maîtriser le traitement des conversions entre les différentes générations de temps présentes dans Windows.

Ne vous faites pas duper à nouveau

Supposons que vous ayez adopté la stratégie « nous stockons tout dans l’heure UCT », probablement pour éviter la surcharge liée au stockage d’un décalage de fuseau horaire (et peut-être à une vue d’utilisateur du fuseau horaire, comme l’heure standard du Pacifique ou PST). Il existe plusieurs avantages à effectuer des calculs à l’aide du temps UCT. Le principal est le fait que, lorsqu’il est représenté en temps universel, chaque jour a une longueur fixe et qu’il n’y a pas de décalages de fuseau horaire à traiter.

Si vous avez été surpris de lire qu’une journée peut avoir des longueurs différentes, sachez que dans n’importe quel fuseau horaire qui permet l’heure d’été, deux jours de l’année (généralement), les jours ont une longueur différente. Par conséquent, même si vous utilisez une valeur d’heure locale, telle que l’heure standard du Pacifique (PST), si vous essayez d’ajouter un intervalle de temps à une valeur DateTime instance spécifique, vous risquez de ne pas obtenir le résultat que vous pensiez que vous devriez si l’intervalle ajouté vous fait passer au-delà de l’heure de changement à une date à laquelle l’heure d’été commence ou se termine.

Examinons un exemple de code qui ne fonctionne pas dans le fuseau horaire pacifique dans le États-Unis :

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)

Le résultat affiché à partir de ce calcul peut sembler correct à première vue; toutefois, le 26 octobre 2003, une minute après 1 h 59 PST, le changement d’heure d’été est entré en vigueur. La réponse correcte aurait dû être 26/10/2003, 02:00:00 AM, de sorte que ce calcul basé sur une valeur d’heure locale n’a pas pu produire le résultat correct. Mais si nous regardons en arrière à la règle 3, nous semblent avoir une contradiction, mais nous ne le faisons pas. Appelons-le simplement un cas particulier pour l’utilisation des méthodes Ajouter/Soustraire dans les fuseaux horaires qui célèbrent l’heure d’été.

Meilleure pratique #3

Lors du codage, soyez prudent si vous devez effectuer des calculs DateTime (ajouter/soustraire) sur des valeurs représentant des fuseaux horaires qui pratiquent l’heure d’été. Des erreurs de calcul inattendues peuvent se produire. Au lieu de cela, convertissez la valeur d’heure locale en temps universel, effectuez le calcul et convertissez-le pour obtenir une précision maximale.

La résolution de ce code défectueux est simple :

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)
  

Le moyen le plus simple d’ajouter de manière fiable des intervalles de temps consiste à convertir des valeurs basées sur le temps local en temps universel, à effectuer les calculs, puis à convertir les valeurs.

Tri des méthodes DateTime

Tout au long de cet article, différentes méthodes de classe System.DateTime sont abordées. Certains produisent un résultat correct lorsque le instance sous-jacent représente l’heure locale, d’autres quand ils représentent l’heure universelle, tandis que d’autres ne nécessitent toujours aucune instance sous-jacente. En outre, certaines sont complètement indépendantes du fuseau horaire (par exemple, AddYear, AddMonth). Pour simplifier la compréhension globale des hypothèses derrière les méthodes de prise en charge DateTime les plus fréquemment rencontrées, le tableau suivant est fourni.

Pour lire le tableau, tenez compte du point de vue de début (entrée) et de fin (valeur retournée). Dans tous les cas, l’état final de l’appel d’une méthode est retourné par la méthode . Aucune conversion n’est effectuée vers le instance de données sous-jacent. Des mises en garde qui décrivent des exceptions ou des conseils utiles sont également fournies.

Nom de la méthode Point de départ Point de vue de fin Mises en garde
ToUniversalTime Heure locale UTC N’appelez pas sur un instance DateTime qui représente déjà l’heure universelle
ToLocalTime UTC Heure locale N’appelez pas sur un instance DateTime qui représente déjà l’heure locale
ToFileTime Heure locale   La méthode retourne une valeur INT64 qui représente l’heure de fichier Win32 (heure UCT)
FromFileTime   Heure locale Méthode statique : aucune instance requise. Prend un temps UCT INT64 comme entrée
ToFileTimeUtc

(V1.1 uniquement)

UTC   La méthode retourne une valeur INT64 qui représente une heure de fichier Win32 (heure UCT)
FromFileTimeUtc

(V1.1 uniquement)

  UTC La méthode convertit l’heure du fichier INT64 Win32 en un instance DateTime UCT
maintenant   Heure locale Méthode statique : aucune instance requise. Retourne un DateTime qui représente l’heure actuelle dans Heure de l’ordinateur local
UtcNow   UTC Méthode statique : aucune instance requise
IsLeapYear Heure locale   Renvoie une valeur booléenne qui indique true si la partie année de l’heure locale instance est une année bissextile.
Aujourd’hui   Heure locale Méthode statique : aucune instance requise. Retourne un DateTime défini sur Minuit du jour actuel dans l’heure de l’ordinateur local.

Cas particulier de XML

Plusieurs personnes avec lesquelles j’ai parlé récemment avaient pour objectif de conception de sérialiser les valeurs de temps sur les services Web de sorte que le XML qui représente le DateTime soit mis en forme en GMT (par exemple, avec un décalage zéro). Bien que j’ai entendu diverses raisons allant du désir d’analyser simplement le champ en tant que chaîne de texte pour l’afficher dans un client à vouloir conserver les hypothèses « stockées dans UCT » qui existent sur le serveur aux appelants des services Web, je n’ai pas été convaincu qu’il n’y a jamais une bonne raison de contrôler le format de marshaling sur le câble à ce degré. Pourquoi ? Tout simplement parce que l’encodage XML d’un type DateTime est parfaitement approprié pour représenter un instant dans le temps, et que le sérialiseur XML intégré au .NET Framework fait un bon travail de gestion des problèmes de sérialisation et de désérialisation associés aux valeurs de temps.

En outre, il s’avère que forçant le System.XML. Le sérialisateur de sérialisation pour encoder une valeur de date en GMT sur le réseau n’est pas possible dans .NET, du moins pas aujourd’hui. En tant que programmeur, concepteur ou gestionnaire de projet, votre travail s’assure ensuite que les données transmises dans votre application sont exécutées avec précision avec un coût minimal.

Plusieurs des groupes avec lesquels j’ai parlé dans la recherche qui a été consacrée à ce document avaient adopté la stratégie de définition de classes spéciales et d’écriture de leurs propres sérialiseurs XML afin qu’ils aient un contrôle total sur ce à quoi ressemblaient les valeurs DateTime sur le réseau dans leur XML. Bien que j’admire la volonté des développeurs de faire le saut dans cette entreprise courageuse, soyez assuré que les nuances de traitement des problèmes d’heure d’été et de conversion de fuseau horaire seuls devraient faire dire à un bon gestionnaire: « Pas moyen », en particulier lorsque les mécanismes fournis dans le .NET Framework font déjà un travail parfaitement précis de sérialisation des valeurs d’heure.

Il n’y a qu’une seule astuce dont vous devez être conscient. En tant que concepteur, vous DEVEZ comprendre cela et respecter la règle (voir Règle n° 5).

Code qui ne fonctionne pas :

Commençons par définir une classe XML simple avec une variable membre DateTime. Pour être complète, cette classe est l’équivalent simplifié de l’approche recommandée illustrée plus loin dans l’article.

<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

Maintenant, nous allons utiliser cette classe pour écrire du code XML dans un fichier.

' 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

Lorsque ce code s’exécute, le code XML sérialisé dans le fichier de sortie contient une représentation DateTime XML comme suit :

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

Il s’agit d’une erreur : la valeur encodée dans le code XML est désactivée de huit heures ! Étant donné qu’il s’agit du décalage de fuseau horaire de mon ordinateur actuel, nous devrions être suspects. En examinant le code XML lui-même, la date est correcte et la date de 20:01:02 correspond à l’heure d’horloge à Londres pour mon propre midi, mais la partie décalage n’est pas correcte pour une horloge basée à Londres. Lorsque le code XML ressemble à l’heure de Londres, le décalage doit également représenter le point de vue de Londres, ce que ce code n’atteint pas.

Le sérialiseur XML suppose toujours que les valeurs DateTime en cours de sérialisation représentent l’heure de l’ordinateur local, de sorte qu’il applique le décalage de fuseau horaire local de l’ordinateur comme partie décalage de l’heure XML encodée. Lorsque nous désérialisons cette valeur sur une autre machine, le décalage d’origine est soustrait de la valeur analysée et le décalage de fuseau horaire de l’ordinateur actuel est ajouté.

Lorsque nous commençons avec une heure locale, le résultat de la sérialisation (encoder en XML DateTime suivi du décodage sur l’heure de l’ordinateur local) est toujours correct, mais uniquement si la valeur DateTime de début en cours de sérialisation représente l’heure locale au début de la sérialisation. Dans le cas de cet exemple de code défectueux, nous avions déjà ajusté la valeur DateTime dans la variable de membre timeVal sur uct time. Par conséquent, lorsque nous sérialisons et désérialisons, le résultat est désactivé par le nombre d’heures égal au décalage de fuseau horaire de l’ordinateur d’origine. C’est mauvais.

Meilleure pratique #4

Lors du test, calculez la valeur que vous prévoyez de voir dans la chaîne XML sérialisée à l’aide d’une vue d’heure locale de l’ordinateur du point dans le temps testé. Si le code XML du flux de sérialisation diffère, enregistrez un bogue !

La résolution de ce code est simple. Commentez la ligne qui appelle ToUniversalTime().

Meilleure pratique n° 5

Lors de l’écriture de code pour sérialiser des classes qui ont des variables de membre DateTime, les valeurs doivent représenter l’heure locale. S’ils ne contiennent pas d’heure locale, ajustez-les avant toute étape de sérialisation, y compris le passage ou le retour de types qui contiennent des valeurs DateTime dans les services Web.

Le problème des codeurs de classe

Plus tôt, nous avons examiné une classe assez peu soignée qui exposait une propriété DateTime. Dans cette classe, nous avons simplement sérialisé ce que nous avons stocké dans un DateTime, sans tenir compte du point de vue de l’heure locale ou universelle. Examinons une approche plus sophistiquée qui offre aux programmeurs un choix principal quant aux hypothèses de fuseau horaire qu’ils désirent, tout en sérialisant toujours correctement.

Lors du codage d’une classe qui aura une variable membre de type DateTime, un programmeur a le choix de rendre la variable membre publique ou d’écrire la logique de propriété pour encapsuler la variable membre avec des opérations get/set . Le choix de rendre le type public présente plusieurs inconvénients qui, dans le cas des types DateTime, peuvent avoir des conséquences qui ne sont pas sous le contrôle du développeur de classe.

En utilisant ce que nous avons appris jusqu’à présent, envisagez plutôt de fournir deux propriétés pour chaque type DateTime.

L’exemple suivant illustre l’approche recommandée pour la gestion des variables membres 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

Cet exemple est l’équivalent corrigé de l’exemple de sérialisation de classe précédente. Dans les deux exemples de classe (celui-ci et le précédent), les classes sont des implémentations décrites avec le schéma suivant :

<?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>

Dans ce schéma et dans toutes les implémentations de classe, nous définissons une variable membre qui représente une valeur de temps facultative. Dans notre exemple recommandé, nous avons fourni deux propriétés avec des getters et des setters : une pour l’heure universelle et l’autre pour l’heure locale. Les attributs entre crochets que vous voyez dans le code indiquent au sérialiseur XML d’utiliser la version de l’heure locale pour la sérialisation et, en général, que l’implémentation de classe aboutit à une sortie conforme au schéma. Pour que la classe traite correctement le manque d’expression facultatif lorsqu’aucune valeur n’est définie dans le instance, la variable timeValSpecified et la logique associée dans la propriété setter contrôlent si l’élément XML est exprimé au moment de la sérialisation ou non. Ce comportement facultatif exploite une fonctionnalité du sous-système de sérialisation qui a été conçu pour prendre en charge le contenu XML facultatif.

L’utilisation de cette approche de la gestion des valeurs DateTime dans vos classes .NET vous offre le meilleur des deux mondes : vous bénéficiez d’un accès au stockage basé sur le temps universel afin que les calculs soient précis et vous obtenez une sérialisation appropriée des vues d’heure locale.

Meilleure pratique n° 6

Lors du codage, rendez les variables membres DateTime privées et fournissez deux propriétés pour manipuler vos membres DateTime en temps local ou universel. Biaisez le stockage dans le membre privé en tant que temps UCT en contrôlant la logique dans vos getters et setters. Ajoutez les attributs de sérialisation XML à la déclaration de propriété d’heure locale pour vous assurer que la valeur d’heure locale est celle qui est sérialisée (voir l’exemple).

Mises en garde à cette approche

L’approche recommandée de la gestion d’un DateTime en temps universel au sein de vos variables membres privées est saine, tout comme la recommandation de fournir des propriétés doubles pour permettre aux codeurs de gérer les versions de temps avec lesquelles ils sont le plus à l’aise. Un problème qu’un développeur utilisant cette approche ou toute autre qui expose n’importe quelle heure locale à un programme continue à être le problème de 25 heures par jour autour de l’heure d’été. Cela continuera d’être un problème pour les programmes qui utilisent CLR version 1.0 et 1.1. Vous devez donc savoir si votre programme tombe dans ce cas particulier (l’heure ajoutée ou manquante pour l’heure représentée) et ajuster manuellement. Pour ceux qui ne peuvent pas tolérer une fenêtre de problème d’une heure par an, la recommandation actuelle est de stocker vos dates sous forme de chaînes ou d’une autre approche autogérée. (Les entiers longs Unix sont une bonne option.)

Pour CLR version 2.0 (disponible dans la prochaine version de Visual Studio® nommée « Whidbey »), la prise en compte de l’ajout d’une valeur DateTime à une heure locale ou universelle est en cours d’ajout au .NET Framework. À ce stade, le modèle recommandé continuera de fonctionner, mais pour les programmes qui interagissent avec les variables membres via les propriétés UTC, ces erreurs dans la période d’heures manquantes/supplémentaires seront éliminées. Pour cette raison, la meilleure pratique pour le codage à l’aide de propriétés doubles est fortement suggérée aujourd’hui, afin que vos programmes migrent correctement vers CLR version 2.0.

Gestion de l’heure d’été

Alors que nous nous préparons à fermer et à quitter le sujet des pratiques de codage et de test pour les valeurs DateTime, il reste un cas particulier que vous devez comprendre. Ce cas implique les ambiguïtés qui entourent l’heure d’été et le problème répété d’une heure par an. Ce problème est principalement un problème qui affecte uniquement les applications qui collectent des valeurs de temps à partir de l’entrée utilisateur.

Pour ceux d’entre vous qui appartiennent à la majorité des pays, ce cas est trivial, car dans la plupart des pays, l’heure d’été n’est pas pratiquée. Mais pour ceux d’entre vous qui font partie de la majorité des programmes touchés (c’est-à-dire, vous tous qui avez des demandes qui ont besoin de gérer le temps qui peut être représenté dans des endroits qui pratiquent l’épargne d’été), vous devez savoir que ce problème existe et en tenir compte.

Dans les régions du monde qui pratiquent l’heure d’été, il y a une heure à l’automne et au printemps où le temps semble s’embraser. La nuit où l’heure de l’horloge passe de l’heure standard à l’heure d’été, l’heure avance d’une heure. Cela se produit au printemps. À l’automne de l’année, une nuit, l’horloge locale saute en arrière d’une heure.

Ces jours-ci, vous pouvez rencontrer des conditions où la journée est de 23 ou 25 heures. Par conséquent, si vous ajoutez ou soustrayez des intervalles de temps à partir de valeurs de date et que l’intervalle traverse ce point étrange dans le temps où les horloges basculent, votre code doit effectuer un ajustement manuel.

Pour la logique qui utilise la méthode DateTime.Parse() pour calculer une valeur DateTime en fonction de l’entrée utilisateur d’une date et d’une heure spécifiques, vous devez détecter que certaines valeurs ne sont pas valides (sur le jour de 23 heures), et que certaines valeurs ont deux significations, car une heure particulière se répète (sur la journée de 25 heures). Pour ce faire, vous devez connaître les dates impliquées et rechercher ces heures. Il peut être utile d’analyser et de réafficher les informations de date interprétées lorsque l’utilisateur quitte les champs utilisés pour entrer des dates. En règle générale, évitez que les utilisateurs spécifient l’heure d’été dans leur entrée.

Nous avons déjà abordé les meilleures pratiques pour les calculs d’intervalle de temps. En convertissant vos affichages de temps local en temps universel avant d’effectuer vos calculs, vous dépassez les problèmes de précision de l’heure. Le cas le plus difficile à gérer est le cas d’ambiguïté associé à l’analyse des entrées utilisateur qui se produit pendant cette heure magique au printemps et à l’automne.

Actuellement, il n’existe aucun moyen d’analyser une chaîne qui représente l’affichage de l’heure d’un utilisateur et de lui attribuer avec précision une valeur de temps universelle. La raison en est que les personnes qui connaissent l’heure d’été n’habitent pas dans des endroits où le fuseau horaire est l’heure moyenne de Greenwich. Ainsi, il est tout à fait possible qu’une personne vivant sur la côte est de la États-Unis des types dans une valeur comme « Oct 26, 2003 01:10:00 AM ».

En ce matin particulier, à 2:00, l’horloge locale est réinitialisée à 1:00, ce qui crée une journée de 25 heures. Étant donné que toutes les valeurs de l’heure de l’horloge comprises entre 1 h 00 et 2 h 00 se produisent deux fois ce matin-là, du moins dans la plupart des États-Unis et au Canada. L’ordinateur n’a vraiment aucun moyen de savoir qui était prévu à 1 h 10 , celui qui se produit avant le commutateur, ou celui qui se produit 10 minutes après le changement d’heure d’été.

De même, vos programmes doivent faire face au problème qui se produit au printemps lorsque, un matin donné, il n’y a pas de temps comme 2 h 10. La raison en est qu’à 2:00 ce matin particulier, l’heure sur les horloges locales change soudainement à 3:00 AM. L’intégralité de l’heure 2:00 ne se produit jamais sur ce jour de 23 heures.

Vos programmes doivent faire face à ces cas, éventuellement en invitant l’utilisateur lorsque vous détectez l’ambiguïté. Si vous ne collectez pas les chaînes date-heure des utilisateurs et ne les analysez pas, vous ne rencontrez probablement pas ces problèmes. Les programmes qui doivent déterminer si une heure particulière se situe à l’heure d’été peuvent utiliser les éléments suivants :

Timezone.CurrentTimeZone.IsDaylightsavingTime(DateTimeInstance)

or

DateTimeInstance.IsDaylightsavingTime

Meilleure pratique n° 7

Lors du test, si vos programmes acceptent des entrées utilisateur spécifiant des valeurs de date et d’heure, veillez à tester la perte de données sur « spring-ahead », « fall-back » jours 23 et 25 heures. Veillez également à tester les dates collectées sur un ordinateur dans un fuseau horaire et stockées sur un ordinateur dans un autre fuseau horaire.

Mise en forme et analyse des valeurs User-Ready

Pour les programmes qui prennent des informations de date et d’heure des utilisateurs et qui doivent convertir cette entrée utilisateur en valeurs DateTime, l’infrastructure prend en charge les chaînes d’analyse mises en forme de manière spécifique. En général, les méthodes DateTime.Parse et ParseExact sont utiles pour convertir des chaînes contenant des dates et des heures en valeurs DateTime. À l’inverse, les méthodes ToString, ToLongDateString, ToLongTimeString, ToShortDateString et ToShortTimeString sont toutes utiles pour le rendu des valeurs DateTime en chaînes lisibles par l’homme.

Deux main problèmes qui affectent l’analyse sont la culture et la chaîne de format. Les questions fréquentes (FAQ) DateTime couvrent les problèmes de base liés à la culture. Nous allons donc nous concentrer ici sur les meilleures pratiques en matière de chaîne de format qui affectent l’analyse DateTime.

Les chaînes de format recommandées pour la conversion de DateTime en chaînes sont les suivantes :

'yyyy'-'MM'-'dd’T’HH': 'mm': 'ss.fffffff’Z' — Pour les valeurs UCT

'yyyy'-'MM'-'dd’T’HH': 'mm': 'ss.fffffff’zzz' — Pour les valeurs locales

'yyyy'-'MM'-'dd’T’HH': 'mm': 'ss.fffffff' — Pour les valeurs temporelles abstraites

Il s’agit des valeurs de chaîne de format qui seraient passées à la méthode DateTime.ToString si vous souhaitez obtenir une sortie compatible avec la spécification de type DateTime XML. Les guillemets permettent de s’assurer que les paramètres date-heure locaux sur l’ordinateur ne remplacent pas vos options de mise en forme. Si vous devez spécifier différentes dispositions, vous pouvez passer d’autres chaînes de format pour une fonctionnalité de rendu de date assez flexible, mais veillez à utiliser uniquement la notation Z pour afficher les chaînes à partir de valeurs UCT et à utiliser la notation zzz pour les valeurs d’heure locales.

L’analyse des chaînes et leur conversion en valeurs DateTime peuvent être effectuées avec les méthodes DateTime.Parse et ParseExact. Pour la plupart d’entre nous, Parse est suffisant, car ParseExact vous oblige à fournir votre propre objet Formateur instance. L’analyse est assez flexible et capable de convertir avec précision la plupart des chaînes qui contiennent des dates et des heures.

Enfin, il est important d’appeler toujours les méthodes Parse et ToString uniquement après avoir défini cultureinfo du thread sur CultureInfo.InvariantCulture.

Considérations futures

Une chose que vous ne pouvez pas faire facilement avec DateTime.ToString est de mettre en forme une valeur DateTime dans un fuseau horaire arbitraire. Cette fonctionnalité est à l’étude pour les implémentations futures du .NET Framework. Si vous devez être en mesure de déterminer que la chaîne « 12:00:00 EST » équivaut à « 11:00:00 EDT », vous devez gérer la conversion et la comparaison vous-même.

Problèmes liés à la méthode DateTime.Now()

Il existe plusieurs problèmes lors de la gestion de la méthode nommée Now. Pour les développeurs Visual Basic qui lisent ceci, cela s’applique également à la fonction Visual Basic Now . Les développeurs qui utilisent régulièrement la méthode Now savent qu’elle est couramment utilisée pour obtenir l’heure actuelle. La valeur retournée par la méthode Now se trouve dans le contexte de fuseau horaire actuel de l’ordinateur et ne peut pas être traitée comme une valeur immuable. Une pratique courante consiste à convertir les heures qui vont être stockées ou envoyées entre les machines en heure universelle (UCT).

Lorsque l’heure d’été est possible, il existe une pratique de codage que vous devez éviter. Considérez le code suivant qui peut introduire un bogue difficile à détecter :

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

La valeur résultant de l’exécution de ce code sera désactivée d’une heure si elle est appelée pendant l’heure supplémentaire qui se produit pendant le changement d’heure d’été à l’automne. (Cela s’applique uniquement aux machines qui se trouvent dans des fuseaux horaires qui pratiquent l’heure d’été.) Étant donné que l’heure supplémentaire se situe à l’endroit où la même valeur, par exemple 01:10:00, se produit deux fois ce matin-là, la valeur retournée peut ne pas correspondre à la valeur souhaitée.

Pour résoudre ce problème, une bonne pratique consiste à appeler DateTime.UtcNow() au lieu d’appeler DateTime.Now, puis à convertir en heure universelle.

Dim timeval As DateTime
timeval = DateTime.UtcNow()  

Ce code aura toujours la perspective appropriée 24 heures sur 24 et peut ensuite être converti en toute sécurité en heure locale.

Meilleure pratique n° 8

Lorsque vous codez et que vous souhaitez stocker l’heure actuelle représentée en tant qu’heure universelle, évitez d’appeler DateTime.Now() suivi d’une conversion en heure universelle. Au lieu de cela, appelez directement la fonction DateTime.UtcNow.

Mise en garde : si vous envisagez de sérialiser une classe qui contient une valeur DateTime, assurez-vous que la valeur en cours de sérialisation ne représente pas le temps universel. La sérialisation XML ne prend pas en charge la sérialisation UCT jusqu’à la publication whidbey de Visual Studio.

Quelques extras peu connus

Parfois, lorsque vous commencez à plonger dans une partie d’une API, vous trouvez un bijou caché, quelque chose qui vous aide à atteindre un objectif, mais qui, si vous n’en êtes pas informé, vous ne découvrirez pas dans vos voyages quotidiens. Le type de valeur DateTime dans .NET comporte plusieurs gemmes de ce type qui peuvent vous aider à obtenir une utilisation plus cohérente du temps universel.

La première est l’énumération DateTimeStyles qui se trouve dans l’espace de noms System.Globalization . L’énumération contrôle les comportements des fonctions DateTime.Parse() et ParseExact utilisées pour convertir des entrées spécifiées par l’utilisateur et d’autres formes de représentations sous forme de chaîne d’entrée en valeurs DateTime.

Le tableau suivant met en évidence certaines des fonctionnalités que l’énumération DateTimeStyles active.

Constante d’énumération Objectif Mises en garde
AdjustToUniversal Lorsqu’il est passé dans le cadre d’une méthode Parse ou ParseExact, cet indicateur entraîne le retour de la valeur en temps universel. La documentation est ambiguë, mais elle fonctionne avec Parse et ParseExact.
NoCurrentDateDefault Supprime l’hypothèse que les chaînes analysées sans composant de date auront une valeur DateTime retournée qui correspond à l’heure de la date actuelle. Si cette option est utilisée, la valeur DateTime retournée est l’heure spécifiée à la date grégorienne 1er janvier de l’année 1.
AllowWhiteSpaces

AllowTrailingWhite

AllowLeadingWhite

AllowInnerWhite

Ces options activent toutes la tolérance pour les espaces blancs ajoutés devant, derrière et au milieu des chaînes de date analysées. None

D’autres fonctions de prise en charge intéressantes se trouvent dans la classe System.Timezone . Veillez à les case activée si vous souhaitez détecter si l’heure d’été affectera une valeur DateTime, ou si vous souhaitez déterminer par programmation le décalage de fuseau horaire actuel pour l’ordinateur local.

Conclusion

La classe DateTime .NET Framework fournit une interface complète pour l’écriture de programmes qui traitent le temps. Comprendre les nuances de la gestion de la classe va au-delà de ce que vous pouvez glaner à partir d’IntelliSense®. Ici, nous avons abordé les meilleures pratiques pour le codage et le test des programmes qui traitent les dates et l’heure. Codez bien !