在.NET Framework中使用 DateTime 编码最佳做法

 

丹·罗杰斯
Microsoft Corporation

2004 年 2 月

适用于
   Microsoft® .NET Framework
   Microsoft® ASP.NET Web 服务
   XML 序列化

总结:编写使用 Microsoft .NET Framework中的 DateTime 类型存储、执行计算和序列化时间值的程序需要了解与 Windows 和 .NET 中可用的时间表示形式相关的不同问题。 本文重点介绍涉及时间的关键测试和开发方案,并定义了在 Microsoft 中编写使用 DateTime 类型的程序的最佳做法建议。基于 NET 的应用程序和程序集。 ) (18 页打印页

目录

背景
   无论如何,什么是 DateTime?
   规则
存储策略
   最佳做法 #1
   最佳做法 #2
执行计算
   不再被愚弄
   最佳做法 #3
   排序日期时间方法
XML 的特殊情况
   最佳做法 #4
   最佳做法 #5
类编码员 Quandary
   最佳做法 #6
处理夏令时
   最佳做法 #7
格式化和分析User-Ready值
   未来注意事项
DateTime.Now () 方法的问题
   最佳做法 #8
一些鲜为人知的额外内容
结论

背景

许多程序员遇到要求他们准确存储和处理包含日期和时间信息的数据的作业。 乍一看,公共语言运行时 (CLR) DateTime 数据类型似乎非常适合这些任务。 但是,对于程序员(但更可能是测试人员)来说,遇到程序只是丢失正确时间值的情况并不少见。 本文重点介绍与涉及 DateTime 的逻辑相关的问题,并在此过程中发现编写和测试捕获、存储、检索和传输 DateTime 信息的程序的最佳做法。

无论如何,什么是 DateTime?

查看 NET Framework 类库文档时,可以看到“CLR System.DateTime 值类型表示从 AD 1 月 1 日午夜 12:00:00 到 AD 9999 年 12 月 31 日晚上 11:59:59。”进一步阅读,我们毫不奇怪地了解到,DateTime 值表示某个时间点的瞬间,一种常见做法是在协调世界时 (UCT) 中记录时间点值(通常称为格林威治标准时间 (GMT) )。

首先,程序员发现 DateTime 类型非常善于存储当前编程问题(例如在业务应用程序中)中可能遇到的时间值。 有了这种信心,许多毫无戒心的程序员开始编码,相信他们可以学到尽可能多的时间,因为他们可以继续学习。 这种“即用即学”方法可能会导致你遇到一些问题,因此让我们开始确定它们。 它们的范围从文档中的问题到需要纳入程序设计的行为。

System.DateTime 的 V1.0 和 1.1 文档进行了一些通用化,可能会让毫无戒心的程序员偏离轨道。例如,文档目前仍然显示,在 DateTime 类中找到的方法和属性始终使用假设值表示本地计算机本地时区进行计算或比较。 这种通用化被证明是不真实的,因为有某些类型的日期和时间计算假定 GMT,而另一些类型则假定本地时区视图。 本文稍后会指出这些方面。

因此,让我们首先通过概述一系列规则和最佳做法来探索 DateTime 类型,这些规则和最佳做法可以帮助你使代码第一次正常运行。

规则

  1. 仅当比较或使用的实例是从同一时区角度表示时间点时,DateTime 实例的计算和比较才有意义。
  2. 开发人员负责通过某些外部机制跟踪与 DateTime 值关联的时区信息。 通常,这是通过定义另一个字段或变量来实现的,在存储 DateTime 值类型时,该字段或变量用于记录时区信息。 此方法 (将时区感知与 DateTime 值) 存储在一起是最准确的方法,它允许程序生命周期中不同点的不同开发人员始终清楚地了解 DateTime 值的含义。 另一种常见方法是将其作为设计中的“规则”,即所有时间值都存储在特定的时区上下文中。 此方法不需要额外的存储来保存用户对时区上下文的视图,但会带来时间值被不了解规则的开发人员错误解释或错误存储的风险。
  3. 对表示计算机本地时间的值执行日期和时间计算可能并不总是产生正确的结果。 在采用夏令时制的时区上下文中对时间值执行计算时,应在执行日期算术计算之前将值转换为通用时间表示形式。 有关操作的特定列表和适当的时区上下文,请参阅排序日期时间方法部分中的表。
  4. 对 DateTime 值的实例进行计算不会修改实例的值,因此对 MyDateTime.ToLocalTime () 的调用不会修改 DateTime 实例的值。 与 Visual Basic®) 中的 Date (和 .NET CLR) 类中的 DateTime (关联的方法返回表示计算或操作结果的新实例。
  5. 使用 .NET Framework版本 1.0 和 1.1 时,请勿发送表示 UCT 时间到System.XML 的 DateTime 值。序列化。 这适用于 Date、Time 和 DateTime 值。 对于涉及 System.DateTime 的 XML 的 Web 服务和其他形式的序列化,始终确保 DateTime 值中的值表示当前计算机本地时间。 序列化程序将正确解码 XML 架构定义的 DateTime 值,该值以 GMT (偏移值 = 0) 编码,但会将其解码为本地计算机时间视点。
  6. 一般情况下,如果要处理绝对已用时间,例如测量超时、执行算术或比较不同的 DateTime 值,应尽可能尝试使用通用时间值,以便在不影响时区和/或夏令时的影响下获得尽可能准确的值。
  7. 在处理面向用户的高级概念(如计划)时,可以安全地假设每天有 24 小时(从用户的角度来看),可以通过在本地时间执行算术等操作来反击规则 #6。

在本文中,此简单的规则列表是编写和测试处理日期的应用程序的一组最佳做法的基础。

到目前为止,你们中的一些人已经在查看代码,并说,“哦,达恩,它并没有按照我的期望执行,”这就是本文的目的。 对于我们这些尚未从阅读到此为止尚未顿悟的人,让我们看看从现在开始 (处理 DateTime 值相关的问题,我只需将它缩短为“date”) 。基于 NET 的应用程序。

存储策略

根据上述规则,只有在了解与正在处理的日期值关联的时区信息时,对日期值的计算才有意义。 这意味着,无论你是将值临时存储在类成员变量中,还是选择将收集的值保存到数据库或文件中,你作为程序员都有责任应用一种策略,该策略允许稍后了解关联的时区信息。

最佳做法 #1

编码时,将与 DateTime 类型关联的时区信息存储在辅助变量中。

另一种但不太可靠的策略是制定一个坚定的规则,即存储的日期将始终在存储之前转换为特定的时区,例如 GMT。 这似乎很合理,许多团队都可以使它发挥作用。 但是,缺少指示数据库中表中特定 DateTime 列处于特定时区的明显信号总是会导致在项目的后续迭代中出现解释错误。

在对不同对象的非正式调查中看到的共同策略。基于 NET 的应用程序希望始终在通用 (GMT) 时间表示日期。 我说“欲望”,因为这并不总是可行的。 当通过 Web 服务序列化具有 DateTime 成员变量的类时,会出现一个案例。 原因是 DateTime 值类型映射到 XSD:DateTime 类型 (预期) ,并且 XSD 类型适用于表示任何时区中的时间点。 我们稍后将讨论 XML 案例。 更有趣的是,其中很大一部分项目实际上没有实现其目标,并且将日期信息存储在服务器时区中,而没有意识到这一点。

在这些情况下,一个有趣的事实是,测试人员没有看到时间转换问题,因此没有人注意到,应该将本地日期信息转换为 UCT 时间的代码失败。 在这些特定情况下,数据后来通过 XML 进行序列化并正确转换,因为日期信息是在计算机本地时间开始的。

让我们看一些 不起作用的代码

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

上述程序采用变量 d 中的值,并将其保存到数据库,期望存储的值表示 UCT 时间视图。 此示例识别到 Parse 方法以本地时间呈现结果,除非某些非默认区域性用作 Parse 方法系列的可选参数。

之前显示的代码实际上无法将 DateTime 变量 d 中的值转换为第三行中的通用时间,因为按照编写,示例违反了规则 4 (DateTime 类上的方法不会将基础值转换为) 。 注意:此代码在经过测试的实际应用程序中可见。

它是如何通过的? 所涉及的应用程序能够成功比较存储的日期,因为在测试期间,所有数据都来自设置为同一时区的计算机,因此满足规则 #1, (比较和计算的所有日期都本地化为同一时区) 。 此代码中的 bug 是一种难以发现的 bug -- 一个执行但不执行任何操作的语句 (提示:示例中的最后一个语句是) 编写的 no-op。

最佳做法 #2

测试时,检查查看存储的值表示在所需时区中所需的时间点值

修复代码示例非常简单:

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

由于与 DateTime 值类型关联的计算方法永远不会影响基础值,而是返回计算结果,因此程序必须记住,如果需要, (存储转换后的值,当然) 。 接下来,我们将研究在涉及夏令时的某些情况下,即使这种看似正确的计算也无法实现预期结果。

执行计算

乍一看,System.DateTime 类附带的计算函数非常有用。 支持向时间值添加间隔、对时间值执行算术,甚至将 .NET 时间值转换为适用于 Win32® API 调用以及 OLE 自动化调用的相应值类型。 看看围绕 DateTime 类型的支持方法,可以唤起对 MS-DOS® 和 Windows® 多年来处理时间和时间戳的不同方式的怀旧观念。

所有这些组件仍存在于操作系统的各个部分,这与 Microsoft 维护的向后兼容性要求有关。 对于程序员来说,这意味着,如果要移动表示文件、目录上的时间戳的数据,或执行涉及日期和 DateTime 值的 COM/OLE 互操作,则必须熟练处理 Windows 中存在的不同代时间之间的转换。

不再被愚弄

假设你已采用“我们在 UCT 时间中存储所有内容”策略,这大概是为了避免 (存储时区偏移量的开销,以及可能需要用户关注的时区视图(例如太平洋标准时间或 PST) )。 使用 UCT 时间执行计算有几个优点。 其中主要的事实是,当以普遍时间表示时,每一天都有固定的长度,并且没有时区偏移量需要处理。

如果你惊讶地发现一天可能有不同的长度,请注意,在允许夏令时的任何时区中,一年中的两天 (通常) ,天都有不同的长度。 因此,即使使用的是本地时间值(例如太平洋标准时间 (PST) ),如果尝试将时间跨度添加到特定的 DateTime 实例值,如果添加的间隔使你超过夏令时开始或结束的日期的更改时间,你也可能不会得到你认为应该的结果。

让我们看一个在美国的太平洋时区中不起作用的代码示例

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)

从此计算中显示的结果在第一眼看起来可能是正确的;然而,在 2003 年 10 月 26 日,太平洋时间凌晨 1:59 后一分钟,夏令时更改生效。 正确的答案应该是 2003 年 10 月 26 日上午 02:00:00,因此基于本地时间值的计算未能生成正确结果。 但是,如果我们回顾规则3,我们似乎有一个矛盾,但我们没有。 让我们将其称为在庆祝夏令时时区使用 Add/Subtract 方法的特例。

最佳做法 #3

编码时,如果需要执行 DateTime 计算, (对表示采用夏令时时区的值添加/减去) ,请小心谨慎。 可能导致意外的计算错误。 相反,将本地时间值转换为通用时间,执行计算,然后转换回以实现最大准确性.

修复此损坏的代码非常简单:

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)
  

可靠添加时间跨度的最简单方法是将基于本地时间的值转换为通用时间,执行计算,然后将值转换回去。

排序日期时间方法

本文将讨论不同的 System.DateTime 类方法。 当基础实例表示本地时间时,有些会生成正确的结果,有些则表示通用时间,而另一些则仍不需要任何基础实例。 此外,有些完全不了解时区 (,例如 AddYearAddMonth) 。 为了简化对最常遇到的 DateTime 支持方法背后的假设的总体理解,提供了下表。

若要读取表,请考虑起始 (输入) 和结束 (返回值) 视点。 在所有情况下,调用方法的结束状态都由 方法返回。 不会对数据的基础实例进行转换。 还提供了描述异常或有用指南的注意事项。

方法名 起始视点 结束视点 注意事项
ToUniversalTime 当地时间 UTC 不要对已表示世界时的 DateTime 实例调用
ToLocalTime UTC 当地时间 不要在已表示本地时间的 DateTime 实例上调用
ToFileTime 当地时间   方法返回一个 INT64,表示 Win32 文件时间 (UCT 时间)
FromFileTime   当地时间 静态方法 - 无需任何实例。 将 INT64 UCT 时间用作输入
ToFileTimeUtc

仅) (V1.1

UTC   方法返回一个 INT64,表示 Win32 文件时间 (UCT 时间)
FromFileTimeUtc

仅) (V1.1

  UTC 方法将 INT64 Win32 文件时间转换为 DateTime UCT 实例
Now   当地时间 静态方法 - 无需任何实例。 返回一个 DateTime,该值代表当前时间(以本地计算机时间为单位)
UtcNow   UTC 静态方法 - 无需实例
IsLeapYear 当地时间   返回布尔值,指示如果本地时间实例的年份部分为闰年,则为 true。
今天   当地时间 静态方法 - 无需任何实例。 返回设置为本地计算机时间当天午夜的 DateTime。

XML 的特殊情况

我最近与之交谈的几个人的设计目标是通过 Web 服务序列化时间值,以便表示 DateTime 的 XML 采用 GMT (格式(例如,) 零偏移量)。 虽然我听到过各种原因,从只是将字段解析为要在客户端中显示的文本字符串,到想要保留服务器上存在的“存储在 UCT 中”假设,到 Web 服务的调用方,我一直不相信有充分的理由控制网络上的封送格式到这种程度。 为什么? 仅仅因为 DateTime 类型的 XML 编码完全足以表示即时的时间,而内置于 .NET Framework的 XML 序列化程序在管理与时间值相关的序列化和反序列化问题方面做得非常出色。

此外,事实证明,强制System.XML。在 .NET 中,无法对网络上的 GMT 日期值进行编码的序列化程序,至少目前是无法实现的。 作为程序员、设计人员或项目经理,你的工作会确保以最低成本准确地执行应用程序中传递的数据。

在本文的研究中,我与之交谈的几个组都采用了定义特殊类并编写自己的 XML 序列化程序的策略,以便他们可以完全控制网络上的 DateTime 值在其 XML 中的外观。 虽然我钦佩开发人员在跃进这一勇敢事业时拥有的尝试,但请放心,单独处理夏令时和时区转换问题的细微差别应该让一个优秀的经理说,“没有办法”,特别是当.NET Framework中提供的机制已经非常准确地序列化时间值时。

只有一个技巧需要注意,作为设计人员,你必须了解这一点,并遵守规则 (请参阅规则 #5) 。

不起作用的代码

首先,我们来定义一个包含 DateTime 成员变量的简单 XML 类。 为了完整性,此类是本文后面介绍的建议方法的简化等效项。

<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

现在,让我们使用此类将一些 XML 写入文件。

' 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

运行此代码时,序列化为输出文件的 XML 包含 XML DateTime 表示形式,如下所示:

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

这是一个错误:XML 中编码的值已关闭 8 小时! 由于这恰好是当前计算机的时区偏移量,因此我们应表示怀疑。 从 XML 本身来看,日期是正确的,20:01:02 的日期对应于我自己中午在伦敦的时钟时间,但偏移部分对于基于伦敦的时钟来说不正确。 当 XML 看起来类似于伦敦时间时,偏移量还应表示此代码无法实现的伦敦视点。

XML 序列化程序始终假定要序列化的 DateTime 值表示本地计算机时间,因此它会应用计算机本地时区偏移量作为编码的 XML 时间的偏移量部分。 当我们将其反序列化到另一台计算机上时,将从要分析的值中减去原始偏移量,并添加当前计算机的时区偏移量。

从本地时间开始时,序列化的结果 (编码为 XML DateTime,然后解码为本地计算机时间) 始终正确,但前提是序列化的起始 DateTime 值表示序列化开始时的本地时间。 在此损坏的代码示例中,我们已将 timeVal 成员变量中的 DateTime 值调整为 UCT 时间,因此,当我们序列化和反序列化时,结果会关闭与原始计算机的时区偏移量相等的小时数。 这很糟糕。

最佳做法 #4

测试时,计算预期在 XML 字符串中看到的值,该字符串使用所测试时间点的计算机本地时间视图进行序列化。 如果序列化流中的 XML 不同,请记录 bug!

修复此代码很简单。 注释掉调用 ToUniversalTime () 的行。

最佳做法 #5

编写代码以序列化具有 DateTime 成员变量的类时,值必须表示本地时间。 如果它们不包含本地时间,请在任何序列化步骤之前对其进行调整,包括传递或返回包含 Web 服务中 DateTime 值的类型。

类编码员 Quandary

之前,我们查看了一个相当不老的类,该类公开了 DateTime 属性。 在该类中,我们只是序列化存储在 DateTime 中的内容,而不考虑该值是表示本地时间还是通用时间视点。 让我们来看看一种更复杂的方法,该方法为程序员提供了一个公开的选择,即他们想要什么时区假设,同时始终正确序列化。

编写具有 DateTime 类型成员变量的类时,程序员可以选择公开成员变量或编写属性逻辑以使用 get/set 操作包装成员变量。 选择公开该类型有几个缺点,在 DateTime 类型的情况下,可能会产生不受类开发人员控制的后果。

使用我们到目前为止学到的知识,请考虑为每个 DateTime 类型提供两个属性。

以下示例演示了管理 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

此示例是更正的等效于前面的类序列化示例。 在 (的两个类示例中,以及前面的) 的两个类示例中,类都是使用以下架构描述的实现:

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

在此架构和任何类实现中,我们定义一个表示可选时间值的成员变量。 在建议的示例中,我们提供了两个具有 getter 和 setter 的属性-一个用于通用时间,一个用于本地时间。 代码中显示的带尖括号的属性会告知 XML 序列化程序使用本地时间版本进行序列化,并且通常使类实现产生符合架构的输出。 为了使类在实例中未设置任何值时正确处理可选的表达式缺失,属性 setter 中的 timeValSpecified 变量和关联逻辑控制 XML 元素是否在序列化时表示。 此可选行为利用序列化子系统中的一项功能,该功能旨在支持可选 XML 内容。

使用此方法在 .NET 类中管理 DateTime 值可以充分利用两全其美 -- 你可以根据通用时间获得存储访问权限,以便计算准确,并获取本地时间视图的适当序列化。

最佳做法 #6

编码时,将 DateTime 成员变量设为私有,并提供两个属性,用于在本地或世界时间操作 DateTime 成员。 通过控制 getter 和 setter 中的逻辑,将专用成员中的存储设置为 UCT 时间。 将 XML 序列化属性添加到本地时间属性声明,以确保本地时间值是序列化 (请参阅示例) 。

此方法的注意事项

在专用成员变量中管理通用时间中的 DateTime 的建议方法是合理的,建议提供双重属性,以允许编码人员处理他们最熟悉的时间版本。 开发人员使用此方法或任何其他方法向程序公开任何本地时间的问题仍然是夏令时前后的 25 小时问题。 对于使用 CLR 版本 1.0 和 1.1 的程序来说,这仍然是一个问题,因此,你必须知道程序是否属于此特殊情况, () 表示的时间增加或缺失的小时数,并手动调整。 对于不能容忍每年一小时的问题时段的用户,当前建议将日期存储为字符串或其他一些自我管理的方法。 (Unix 长整数是一个不错的选择。)

对于即将发布的名为“Whidbey”的 Visual Studio® ) 中提供的 CLR 版本 2.0 (,需要了解 DateTime 是包含本地时间还是包含通用时间值,这一.NET Framework。 此时,建议的模式将继续工作,但对于通过 UTC 属性与成员变量交互的程序,将消除缺失/额外一小时期间的这些错误。 因此,现在强烈建议使用双重属性进行编码的最佳做法,以便程序干净地迁移到 CLR 版本 2.0。

处理夏令时

当我们准备结束并离开 DateTime 值的编码和测试实践主题时,仍需了解一个特殊情况。 此案例涉及围绕夏令时和每年重复的一小时问题的含糊不清。 此问题主要影响从用户输入收集时间值的应用程序。

对于国家/地区占多数的你们来说,这种情况是微不足道的,因为在大多数国家/地区不实行夏令时。 但是,对于那些在受影响的计划中,大多数 (,也就是说,你们所有的应用程序都需要处理时间,这些时间可能代表或来源在实践夏令时) 的地方,你必须知道这个问题的存在并解释它。

在世界上实行夏令时的地区,每个秋天和春天都有一个小时的时间似乎去干草线。 当时钟时间从标准时间转移到夏令时时,时间将提前一小时。 这发生在春天。 在一年的秋天,在一个晚上,当地时间时钟跳回一个小时。

在这些日子里,你可能会遇到一天为 23 或 25 小时的条件。 因此,如果要从日期值中添加或减去时间跨度,并且跨度跨越时钟切换的这个奇怪的时间点,则代码需要进行手动调整。

对于使用 DateTime.Parse () 方法根据用户输入的特定日期和时间计算 DateTime 值的逻辑,需要检测某些值在 23 小时) (无效,并且某些值具有两种含义,因为特定小时在 25 小时) (重复。 为此,需要知道所涉及的日期并查找这些小时。 当用户退出用于输入日期的字段时,分析和重新显示解释的日期信息可能会很有用。 通常,避免让用户在其输入中指定夏令时。

我们已经介绍了时间跨度计算的最佳做法。 在执行计算之前,通过将本地时间视图转换为通用时间,可以解决时间准确性问题。 更难管理的情况是与分析用户输入相关的不明确情况,该情况发生在春季和秋季的这个神奇时刻。

目前,无法分析表示用户时间视图并准确为其分配通用时间值的字符串。 原因是经历夏令时的人不会住在时区为格林威治标准时间的地方。 因此,住在美国东海岸的人完全有可能键入类似于“Oct 26, 2003 01:10:00 AM”的值。

在这个特定早晨,凌晨 2:00,本地时钟将重置为凌晨 1:00,从而创建一个 25 小时的一天。 由于上午 1:00 到凌晨 2:00 之间的所有时钟时间值在该特定早晨出现两次,至少在美国和加拿大的大多数地区是这样。 计算机确实无法知道上午 1:10 是指哪个时间,即在切换之前发生的时间,或在夏令时切换 10 分钟后发生的时间。

同样,您的程序必须处理在春季发生的问题,因为在特定早晨没有凌晨 2:10 这样的时间。 原因是在那个早晨 2:00,当地时钟的时间突然更改为凌晨 3:00。 整个 2:00 小时永远不会发生在这 23 小时一天。

程序必须处理这些情况,可能是在检测到歧义时提示用户。 如果未从用户收集日期时间字符串并对其进行分析,则可能没有这些问题。 需要确定特定时间是否在夏令时的程序可以使用以下内容:

Timezone.CurrentTimeZone.IsDaylightSavingTime (DateTimeInstance)

DateTimeInstance.IsDaylightSavingTime

最佳做法 #7

测试时,如果程序接受用户输入指定日期和时间值,请务必在“预进”、“回退”23 小时和 25 小时天测试数据丢失情况。 此外,请确保测试在一个时区的计算机上收集的日期,并存储在另一个时区的计算机上。

格式化和分析User-Ready值

对于从用户获取日期和时间信息并需要将此用户输入转换为 DateTime 值的程序,框架支持分析以特定方式设置格式的字符串。 通常, DateTime.ParseParseExact 方法可用于将包含日期和时间的字符串转换为 DateTime 值。 相反, ToStringToLongDateStringToLongTimeStringToShortDateStringToShortTimeString 方法都可用于将 DateTime 值呈现为人类可读的字符串。

影响分析的两个main问题是区域性和格式字符串。 DateTime 常见问题解答 (常见问题解答) 涵盖了有关区域性的基本问题,因此,我们将重点介绍影响 DateTime 分析的格式字符串最佳做法。

用于将 DateTime 转换为字符串的建议格式字符串为:

'yyyy'-'MM'-'dd'T'HH': 'mm': 'ss.fffffff'Z' — For UCT values

'yyyy'-'MM'-'dd'T'HH': 'mm': 'ss.fffffff'zzz' — For local values

'yyyy'-'MM'-'dd'T'HH': 'mm': 'ss.fffffff' - For abstract time values

如果要获取与 XML DateTime 类型规范兼容的输出,这些格式字符串值将传递给 DateTime.ToString 方法。 引号可确保计算机上的本地日期时间设置不会覆盖格式设置选项。 如果需要指定不同的布局,可以传递其他格式字符串来获得相当灵活的日期呈现功能,但需要注意仅使用 Z 表示法来呈现来自 UCT 值的字符串,并将 zzz 表示法用于本地时间值。

可以使用 DateTime.Parse 和 ParseExact 方法分析字符串并将其转换为 DateTime 值。 对于我们中的大多数人来说,Parse 已经足够了,因为 ParseExact 要求你提供自己的 Formatter 对象实例。 分析功能强大且灵活,可以准确转换包含日期和时间的大多数字符串。

最后,请务必始终在将线程的 CultureInfo 设置为 CultureInfo.InvariantCulture 之后调用 Parse 和 ToString 方法。

未来注意事项

目前使用 DateTime.ToString 无法轻松完成的一件事是将 DateTime 值格式化为任意时区。 此功能正在考虑用于.NET Framework的未来实现。 如果需要能够确定字符串“12:00:00 EST”等效于“11:00:00 EDT”,则必须自行处理转换和比较。

DateTime.Now () 方法的问题

处理名为 Now 的方法时存在几个问题。 对于阅读本文的 Visual Basic 开发人员,这也适用于 Visual Basic Now 函数。 经常使用 Now 方法的开发人员知道它通常用于获取当前时间。 Now 方法返回的值位于当前计算机时区上下文中,不能被视为不可变值。 一种常见做法是将计算机之间存储或发送的时间转换为通用 (UCT) 时间。

如果可能采用夏令时,应避免采用一种编码做法。 请考虑以下代码,该代码可能会引入难以检测的 bug:

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

如果在秋季夏令时切换期间发生的额外一小时内调用,则运行此代码产生的值将关闭一小时。 (这仅适用于采用夏令时制的时区中的计算机。) 因为额外的小时属于同一值(如凌晨 1:10:00)发生两次的位置,因此返回的值可能与所需的值不匹配。

若要解决此问题,最佳做法是调用 DateTime.UtcNow () 而不是调用 DateTime.Now,然后将 转换为世界时。

Dim timeval As DateTime
timeval = DateTime.UtcNow()  

此代码将始终具有正确的 24 小时日透视,然后可以安全地转换为本地时间。

最佳做法 #8

在编码并希望存储表示为世界时的当前时间时时,请避免调用 DateTime.Now () ,然后调用转换为世界时。 而是直接调用 DateTime.UtcNow 函数。

警告:如果要序列化包含 DateTime 值的类,请确保序列化的值不表示通用时间。 在 Visual Studio 的 Whidbey 发布之前,XML 序列化将不支持 UCT 序列化。

几个鲜为人知的额外内容

有时,当你开始深入到 API 的某个部分时,你会发现一个隐藏的宝石,它可以帮助你实现一个目标,但如果你没有被告知它,你就不会在日常旅行中发现。 .NET 中的 DateTime 值类型具有多个此类 gem,可帮助你更一致地使用世界时。

第一个是在 System.Globalization 命名空间中找到的 DateTimeStyles 枚举。 枚举控制 DateTime.Parse () 和 ParseExact 函数的行为,这些函数用于将用户指定的输入和其他形式的输入字符串表示形式转换为 DateTime 值。

下表突出显示了 DateTimeStyles 枚举启用的一些功能。

枚举常量 目的 注意事项
AdjustToUniversal 当作为 Parse 或 ParseExact 方法的一部分传递时,此标志会导致返回的值为世界时。 文档不明确,但这适用于 Parse 和 ParseExact。
NoCurrentDateDefault 禁止假设在没有日期组件的情况下分析的字符串将返回 DateTime 值,该值是当前日期的时间。 如果使用此选项,则返回的 DateTime 值是 1 年 1 月 1 日公历日期中指定的时间。
AllowWhiteSpaces

AllowTrailingWhite

AllowLeadingWhite

AllowInnerWhite

这些选项都允许在所分析的日期字符串前面、后面和中间添加空格。

System.Timezone 类中还有其他有趣的支持函数。 如果要检测夏令时是否会影响 DateTime 值,或者想要以编程方式确定本地计算机的当前时区偏移量,请务必检查。

结论

.NET Framework DateTime 类提供了一个功能齐全的界面,用于编写处理时间的程序。 了解处理 类的细微差别超出了你可以从 Intellisense® 中收集的内容。 在这里,我们介绍了处理日期和时间的编码和测试程序的最佳做法。 祝你编码愉快!