在 DateTime、DateOnly、DateTimeOffset、TimeSpan、TimeOnly 和 TimeZoneInfo 之间进行选择

.NET 应用程序可以多种方式使用日期和时间信息。 日期和时间信息的较常见使用方法包括:

  • 仅反映日期,而时间信息不重要。
  • 仅反映时间,而日期信息不重要。
  • 反映未绑定到特定时间和位置的抽象的日期和时间(例如,国际上大多数连锁店在工作日上午 9:00 开张)。
  • 从 .NET 外部的源中(其中日期和时间信息通常以简单数据类型存储)检索日期和时间信息。
  • 唯一、明确地标识单个时间点。 某些应用程序仅要求主机系统具有明确的日期和时间。 其他应用要求整个系统中均具有明确的日期和时间(即,在意义上,某个系统上序列化的日期可以在世界各地的其他系统上进行反序列化和使用)。
  • 保留多个相关时间(例如请求者的本地时间和服务器接收 Web 请求的时间)。
  • 执行日期和时间算法,可能致使唯一、明确标识单个时间点。

.NET 包括 DateTimeDateOnlyDateTimeOffsetTimeSpanTimeOnlyTimeZoneInfo 类型,这些类型均可用于构建使用日期和时间的应用程序。

注意

本文不讨论 TimeZone,因为 TimeZoneInfo 类中几乎包含它的全部功能。 应尽可能地使用 TimeZoneInfo 类,而不是使用 TimeZone 类。

DateTimeOffset 结构

DateTimeOffset 结构表示日期和时间值,以及指示此值与 UTC 的差异程度的偏移量。 因此,此值始终明确地标识单个时间点。

DateTimeOffset 类型包括 DateTime 类型的所有功能以及时区感知功能。 这使它适用于执行以下操作的应用程序:

  • 唯一、明确地标识单个时间点。 DateTimeOffset 类型可用于明确定义“现在”的含义、记录事务时间、记录系统或应用程序事件时间以及记录创建和修改时间。
  • 执行常规日期和时间算法。
  • 保留多个相关时间,只要这些时间存储为两个单独的值或结构中的两个成员。

注意

DateTimeOffset 值的使用频率比 DateTime 值的更高。 因此,在应用程序开发中应考虑使用 DateTimeOffset 作为默认日期和时间类型。

DateTimeOffset 值不限于特定时区,而可以来自各个时区。 以下示例列出了 DateTimeOffset 值(包括本地太平洋标准时间)可属于的时区。

using System;
using System.Collections.ObjectModel;

public class TimeOffsets
{
   public static void Main()
   {
      DateTime thisDate = new DateTime(2007, 3, 10, 0, 0, 0);
      DateTime dstDate = new DateTime(2007, 6, 10, 0, 0, 0);
      DateTimeOffset thisTime;

      thisTime = new DateTimeOffset(dstDate, new TimeSpan(-7, 0, 0));
      ShowPossibleTimeZones(thisTime);

      thisTime = new DateTimeOffset(thisDate, new TimeSpan(-6, 0, 0));
      ShowPossibleTimeZones(thisTime);

      thisTime = new DateTimeOffset(thisDate, new TimeSpan(+1, 0, 0));
      ShowPossibleTimeZones(thisTime);
   }

   private static void ShowPossibleTimeZones(DateTimeOffset offsetTime)
   {
      TimeSpan offset = offsetTime.Offset;
      ReadOnlyCollection<TimeZoneInfo> timeZones;

      Console.WriteLine("{0} could belong to the following time zones:",
                        offsetTime.ToString());
      // Get all time zones defined on local system
      timeZones = TimeZoneInfo.GetSystemTimeZones();
      // Iterate time zones
      foreach (TimeZoneInfo timeZone in timeZones)
      {
         // Compare offset with offset for that date in that time zone
         if (timeZone.GetUtcOffset(offsetTime.DateTime).Equals(offset))
            Console.WriteLine("   {0}", timeZone.DisplayName);
      }
      Console.WriteLine();
   }
}
// This example displays the following output to the console:
//       6/10/2007 12:00:00 AM -07:00 could belong to the following time zones:
//          (GMT-07:00) Arizona
//          (GMT-08:00) Pacific Time (US & Canada)
//          (GMT-08:00) Tijuana, Baja California
//
//       3/10/2007 12:00:00 AM -06:00 could belong to the following time zones:
//          (GMT-06:00) Central America
//          (GMT-06:00) Central Time (US & Canada)
//          (GMT-06:00) Guadalajara, Mexico City, Monterrey - New
//          (GMT-06:00) Guadalajara, Mexico City, Monterrey - Old
//          (GMT-06:00) Saskatchewan
//
//       3/10/2007 12:00:00 AM +01:00 could belong to the following time zones:
//          (GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna
//          (GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague
//          (GMT+01:00) Brussels, Copenhagen, Madrid, Paris
//          (GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb
//          (GMT+01:00) West Central Africa
Imports System.Collections.ObjectModel

Module TimeOffsets
    Public Sub Main()
        Dim thisTime As DateTimeOffset

        thisTime = New DateTimeOffset(#06/10/2007#, New TimeSpan(-7, 0, 0))
        ShowPossibleTimeZones(thisTime)

        thisTime = New DateTimeOffset(#03/10/2007#, New TimeSpan(-6, 0, 0))
        ShowPossibleTimeZones(thisTime)

        thisTime = New DateTimeOffset(#03/10/2007#, New TimeSpan(+1, 0, 0))
        ShowPossibleTimeZones(thisTime)
    End Sub

    Private Sub ShowPossibleTimeZones(offsetTime As DateTimeOffset)
        Dim offset As TimeSpan = offsetTime.Offset
        Dim timeZones As ReadOnlyCollection(Of TimeZoneInfo)

        Console.WriteLine("{0} could belong to the following time zones:", _
                          offsetTime.ToString())
        ' Get all time zones defined on local system
        timeZones = TimeZoneInfo.GetSystemTimeZones()
        ' Iterate time zones
        For Each timeZone As TimeZoneInfo In timeZones
            ' Compare offset with offset for that date in that time zone
            If timeZone.GetUtcOffset(offsetTime.DateTime).Equals(offset) Then
                Console.WriteLine("   {0}", timeZone.DisplayName)
            End If
        Next
        Console.WriteLine()
    End Sub
End Module
' This example displays the following output to the console:
'       6/10/2007 12:00:00 AM -07:00 could belong to the following time zones:
'          (GMT-07:00) Arizona
'          (GMT-08:00) Pacific Time (US & Canada)
'          (GMT-08:00) Tijuana, Baja California
'       
'       3/10/2007 12:00:00 AM -06:00 could belong to the following time zones:
'          (GMT-06:00) Central America
'          (GMT-06:00) Central Time (US & Canada)
'          (GMT-06:00) Guadalajara, Mexico City, Monterrey - New
'          (GMT-06:00) Guadalajara, Mexico City, Monterrey - Old
'          (GMT-06:00) Saskatchewan
'       
'       3/10/2007 12:00:00 AM +01:00 could belong to the following time zones:
'          (GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna
'          (GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague
'          (GMT+01:00) Brussels, Copenhagen, Madrid, Paris
'          (GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb
'          (GMT+01:00) West Central Africa

输出显示,此示例中的每个日期和时间值至少可以属于三个不同时区。 2007 年 6 月 10 日的 DateTimeOffset 值显示,如果日期和时间值表示夏令时,则其相对于 UTC 的偏移量甚至不一定对应于原始时区的基本 UTC 偏移量或其显示名称中找到的相对于 UTC 的偏移量。 因为单个 DateTimeOffset 值与其时区并非紧密耦合的,因此它无法反映到/从夏令时的时区转换。 这在使用日期和时间运算操纵 DateTimeOffset 值时容易出现问题。 有关如何以考虑时区调整规则的方式执行日期和时间算术运算的讨论,请参阅执行日期和时间算术运算

DateTime 结构

DateTime 值定义特定日期和时间。 它包括 Kind 属性,此属性提供有关日期和时间所属时区的有限信息。 由 DateTimeKind 属性返回的 Kind 值指示 DateTime 值是表示本地时间 (DateTimeKind.Local)、协调世界时 (UTC) (DateTimeKind.Utc) 还是未指定的时间 (DateTimeKind.Unspecified)。

DateTime 结构适用于具有以下一个或多个特征的应用程序:

  • 使用抽象的日期和时间。
  • 使用缺少时区信息的日期和时间。
  • 只使用 UTC 日期和时间。
  • 执行日期和时间算法,但不关注常规结果。 例如,在向特定日期和时间添加六个月的加法运算中,是否将结果调整为夏令时通常并不重要。

除非特定的 DateTime 值表示 UTC,否则日期和时间值通常不明确的或其可移植性受限。 例如,如果 DateTime 值表示本地时间,则在该本地时区内可移植(即,如果在其他系统的相同时区上反序列化此值,它仍然明确标识单个时间点)。 在本地时区之外, DateTime 值可以具有多个解释。 如果值的 Kind 属性是 DateTimeKind.Unspecified,则它的可移植性更低:此时它在相同时区内(可能甚至在首次序列化的同一系统上)是不明确的。 仅当 DateTime 值表示 UTC 时,无论使用此值的系统和时区如何,它都会明确标识单个时间点。

重要

在保存或共享 DateTime 数据时,应使用 UTC 并将 DateTime 值的 Kind 属性设置为 DateTimeKind.Utc

DateOnly 结构

DateOnly 结构表示一个不带时间的特定日期。 它不包含时间部分,因此表示一个从一天开始到一天结束的日期。 此结构非常适合用于存储特定日期,例如出生日期、周年日期、假日或业务相关的日期。

虽然可以在使用 DateTime 的同时忽略时间部分,但与 DateTime 相比,使用 DateOnly 有一些好处:

  • 如果 DateTime 结构被时区偏移,它可能会滚动到上一天或第二天。 DateOnly 不能被时区偏移,并且始终表示所设置的日期。
  • 序列化 DateTime 结构包括时间部分,这可能会掩盖数据的意图。 此外,DateOnly 序列化的数据更少。
  • 当代码与数据库(如 SQL Server)交互时,整个日期通常存储为 date 数据类型,其中不包含时间。 DateOnly 与数据库类型更匹配。

有关 DateOnly 的详细信息,请参阅如何使用 DateOnly 和 TimeOnly 结构

重要

DateOnly 在 .NET Framework 中不可用。

TimeSpan 结构

TimeSpan 结构表示时间间隔。 它的两个典型用途是:

  • 反映两个日期和时间值之间的时间间隔。 例如,两个 DateTime 值相减将返回 TimeSpan 值。
  • 测量运行时间。 例如,Stopwatch.Elapsed 属性返回 TimeSpan 值,此值反映从调用开始测量运行时间的其中一种 Stopwatch 方法开始所经过的时间间隔。

TimeSpan 值还可用于在 DateTime 值反映一个时间而不引用特定的一天时替代它。 这种用法类似于 DateTime.TimeOfDayDateTimeOffset.TimeOfDay 属性,它们返回一个表示未引用日期的时间的 TimeSpan 值。 例如, TimeSpan 结构可用于反映商店每天的开张或打烊时间,还可用来表示任何常规事件发生的时间。

以下示例定义了 StoreInfo 结构,其中包含表示商店开张或打烊时间的 TimeSpan 对象,以及表示商店所在时区的 TimeZoneInfo 对象。 此结构还包含两个方法(IsOpenNowIsOpenAt),用于指示假定用户处于本地时区时其所指定的时间商店是否开张。

using System;

public struct StoreInfo
{
   public String store;
   public TimeZoneInfo tz;
   public TimeSpan open;
   public TimeSpan close;

   public bool IsOpenNow()
   {
      return IsOpenAt(DateTime.Now.TimeOfDay);
   }

   public bool IsOpenAt(TimeSpan time)
   {
      TimeZoneInfo local = TimeZoneInfo.Local;
      TimeSpan offset = TimeZoneInfo.Local.BaseUtcOffset;

      // Is the store in the same time zone?
      if (tz.Equals(local)) {
         return time >= open & time <= close;
      }
      else {
         TimeSpan delta = TimeSpan.Zero;
         TimeSpan storeDelta = TimeSpan.Zero;

         // Is it daylight saving time in either time zone?
         if (local.IsDaylightSavingTime(DateTime.Now.Date + time))
            delta = local.GetAdjustmentRules()[local.GetAdjustmentRules().Length - 1].DaylightDelta;

         if (tz.IsDaylightSavingTime(TimeZoneInfo.ConvertTime(DateTime.Now.Date + time, local, tz)))
            storeDelta = tz.GetAdjustmentRules()[tz.GetAdjustmentRules().Length - 1].DaylightDelta;

         TimeSpan comparisonTime = time + (offset - tz.BaseUtcOffset).Negate() + (delta - storeDelta).Negate();
         return comparisonTime >= open && comparisonTime <= close;
      }
   }
}
Public Structure StoreInfo
    Dim store As String
    Dim tz As TimeZoneInfo
    Dim open As TimeSpan
    Dim close As TimeSpan

    Public Function IsOpenNow() As Boolean
        Return IsOpenAt(Date.Now.TimeOfDay)
    End Function

    Public Function IsOpenAt(time As TimeSpan) As Boolean
        Dim local As TimeZoneInfo = TimeZoneInfo.Local
        Dim offset As TimeSpan = TimeZoneInfo.Local.BaseUtcOffset

        ' Is the store in the same time zone?
        If tz.Equals(local) Then
            Return time >= open AndAlso time <= close
        Else
            Dim delta As TimeSpan = TimeSpan.Zero
            Dim storeDelta As TimeSpan = TimeSpan.Zero

            ' Is it daylight saving time in either time zone?
            If local.IsDaylightSavingTime(Date.Now.Date + time) Then
                delta = local.GetAdjustmentRules(local.GetAdjustmentRules().Length - 1).DaylightDelta
            End If
            If tz.IsDaylightSavingTime(TimeZoneInfo.ConvertTime(Date.Now.Date + time, local, tz))
                storeDelta = tz.GetAdjustmentRules(tz.GetAdjustmentRules().Length - 1).DaylightDelta
            End If
            Dim comparisonTime As TimeSpan = time + (offset - tz.BaseUtcOffset).Negate() + (delta - storeDelta).Negate
            Return (comparisonTime >= open AndAlso comparisonTime <= close)
        End If
    End Function
End Structure

随后, StoreInfo 结构可由客户端代码按如下方式使用。

public class Example
{
   public static void Main()
   {
      // Instantiate a StoreInfo object.
      var store103 = new StoreInfo();
      store103.store = "Store #103";
      store103.tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
      // Store opens at 8:00.
      store103.open = new TimeSpan(8, 0, 0);
      // Store closes at 9:30.
      store103.close = new TimeSpan(21, 30, 0);

      Console.WriteLine("Store is open now at {0}: {1}",
                        DateTime.Now.TimeOfDay, store103.IsOpenNow());
      TimeSpan[] times = { new TimeSpan(8, 0, 0), new TimeSpan(21, 0, 0),
                           new TimeSpan(4, 59, 0), new TimeSpan(18, 31, 0) };
      foreach (var time in times)
         Console.WriteLine("Store is open at {0}: {1}",
                           time, store103.IsOpenAt(time));
   }
}
// The example displays the following output:
//       Store is open now at 15:29:01.6129911: True
//       Store is open at 08:00:00: True
//       Store is open at 21:00:00: True
//       Store is open at 04:59:00: False
//       Store is open at 18:31:00: True
Module Example
    Public Sub Main()
        ' Instantiate a StoreInfo object.
        Dim store103 As New StoreInfo()
        store103.store = "Store #103"
        store103.tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time")
        ' Store opens at 8:00.
        store103.open = new TimeSpan(8, 0, 0)
        ' Store closes at 9:30.
        store103.close = new TimeSpan(21, 30, 0)

        Console.WriteLine("Store is open now at {0}: {1}",
                          Date.Now.TimeOfDay, store103.IsOpenNow())
        Dim times() As TimeSpan = {New TimeSpan(8, 0, 0),
                                    New TimeSpan(21, 0, 0),
                                    New TimeSpan(4, 59, 0),
                                    New TimeSpan(18, 31, 0)}
        For Each time In times
            Console.WriteLine("Store is open at {0}: {1}",
                              time, store103.IsOpenAt(time))
        Next
    End Sub
End Module
' The example displays the following output:
'       Store is open now at 15:29:01.6129911: True
'       Store is open at 08:00:00: True
'       Store is open at 21:00:00: False
'       Store is open at 04:59:00: False
'       Store is open at 18:31:00: False

TimeOnly 结构

TimeOnly 结构表示一天中的时间值,例如每日闹钟或每天吃午餐的时间。 TimeOnly 的范围限制为 00:00:00.0000000 - 23:59:59.9999999,即一天中的特定时间。

在引入 TimeOnly 类型之前,程序员通常使用 DateTime 类型或 TimeSpan 类型来表示特定时间。 但是,使用这些结构来模拟没有日期的时间可能会导致一些问题,而 TimeOnly 可以解决这些问题:

  • TimeSpan 表示经过的时间,例如使用秒表测量的时间。 上限范围超过 29,000 年,它的值可以是负值,表示在时间上向后移动。 负的 TimeSpan 并不表示一天中的特定时间。
  • 如果将 TimeSpan 用作一天中的某个时间,则存在可能将其操作为 24 小时以外的值的风险。 TimeOnly 就没有这种风险。 例如,如果一个员工的工作班次从 18:00 开始且持续 8 小时,那么,向 TimeOnly 结构加上 8 小时将滚动到 2:00。
  • 要将 DateTime 用于一天中的某个时间,需要将任意日期与该时间相关联,然后被忽略。 通常的做法是选择 DateTime.MinValue (0001-01-01) 作为日期,但如果从 DateTime 值中减去小时数,可能会发生 OutOfRange 异常。 TimeOnly 就没有这个问题,因为时间在 24 小时的时间范围内前后滚动。
  • 序列化 DateTime 结构包括日期部分,这可能会掩盖数据的意图。 此外,TimeOnly 序列化的数据更少。

有关 TimeOnly 的详细信息,请参阅如何使用 DateOnly 和 TimeOnly 结构

重要

TimeOnly 在 .NET Framework 中不可用。

TimeZoneInfo 类

TimeZoneInfo class represents any of the Earth's time zones, and enables the conversion of any date and time in one time zone to its equivalent in another time zone. 借助 TimeZoneInfo 类,即可处理日期和时间,以使任何日期和时间值均明确标识单个时间点。 TimeZoneInfo 类也可扩展。 虽然它依赖于为 Windows 系统提供且在注册表中定义的时区信息,但仍然支持创建自定义时区。 它还支持时区信息的序列化和反序列化。

在某些情况下,可能需要进一步的开发工作才可以充分利用 TimeZoneInfo 类。 如果日期和时间值不与其所属的时区紧密耦合,则需要进一步的操作。 除非应用程序可提供某种机制将日期和时间与其关联的时区链接,否则特定日期和时间值很容易与其所属时区失去关联。 链接此信息的一种方法是,定义一个同时包含日期和时间值及其关联时区对象的类或结构。

若要利用 .NET 中的时区支持,必须知道实例化日期和时间对象时其值所属的时区。 通常并不知道该时区,尤其是在 Web 或网络应用中。

另请参阅