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

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

  • 仅反映日期,以便时间信息不重要。
  • 仅反映时间,使日期信息不重要。
  • 反映与特定时间和地点无关的抽象日期和时间(例如,大多数商店在工作日上午 9:00 开放)。
  • 若要从 .NET 外部的源检索日期和时间信息,通常日期和时间信息存储在简单的数据类型中。
  • 唯一且明确地标识单个时间点。 某些应用程序仅要求主机系统具有明确的日期和时间。 其他应用程序要求确保在不同系统之间没有歧义(也就是说,在一个系统上序列化的日期可以在世界上任何地方的另一个系统上有意义地反序列化和使用)。
  • 保留多个相关时间(例如请求者的本地时间和服务器的 Web 请求回执时间)。
  • 执行日期和时间算法,可能致使唯一、明确标识单个时间点。

.NET 包括 DateTimeDateOnlyDateTimeOffsetTimeSpanTimeOnlyTimeZoneInfo类型,所有这些类型都可用于生成使用日期和时间的应用程序。

注释

本文不讨论 TimeZone ,因为它的功能几乎完全合并在类中 TimeZoneInfo 。 尽可能使用 TimeZoneInfo 类而不是 TimeZone 类。

DateTimeOffset 结构

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

DateTimeOffset类型包括DateTime类型的所有功能,并具备时区感知能力。 这使得它适用于以下应用程序:

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

注释

这些值 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($"{offsetTime.ToString()} could belong to the following time zones:");
        // 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($"   {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

输出显示此示例中的每个日期和时间值可以属于至少三个不同的时区。 DateTimeOffset 2007/6/10 的值显示,如果日期和时间值表示夏令时,其与 UTC 的偏移量甚至不一定与原始时区的基本 UTC 偏移量相符,也不一定与其显示名称中的 UTC 偏移量相符。 由于单个 DateTimeOffset 值与其时区不紧密耦合,因此它不能反映时区的转换与夏令时之间的转换。 当使用日期和时间算术对 DateTimeOffset 值进行操作时,这可能导致问题。 有关如何以考虑时区调整规则的方式执行日期和时间算术的讨论,请参阅 使用日期和时间执行算术运算

DateTime 结构

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

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

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

除非特定 DateTime 值表示 UTC,否则日期和时间值在可移植性中通常不明确或有限。 例如,如果某个 DateTime 值表示本地时间,则它可移植到该本地时区(即,如果该值在同一时区的另一个系统上反序列化,该值仍明确标识单个时间点)。 在本地时区之外,该值 DateTime 可以有多个解释。 如果值的 Kind 属性为 DateTimeKind.Unspecified,则它更不便于移植:现在即使在同一时区内也可能变得模糊不清,甚至在最初序列化的同一系统上也是如此。 仅当 DateTime 某个值表示 UTC 时,该值才明确标识单个时间点,而不考虑使用该值的系统或时区。

重要

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

DateOnly 结构

DateOnly 结构表示不带时间的特定日期。 由于它没有时间组件,因此它表示从一天开始到一天结束的日期。 此结构非常适合存储特定日期,例如出生日期、周年日期、假日或与业务相关的日期。

尽管可以使用DateTime而忽略时间组件,但相比DateOnly,使用DateTime有一些好处:

  • 如果 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.TimeOfDay 属性 DateTimeOffset.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 {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 {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.000000000 - 23:59:59.99999999,一天中的特定时间。

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 类表示地球的任何时区,并使一个时区中的任何日期和时间转换为另一个时区中的等效日期和时间。 该 TimeZoneInfo 类使它能够处理日期和时间,以便任何日期和时间值明确标识单个时间点。 该 TimeZoneInfo 类也是可扩展的。 尽管它依赖于为 Windows 系统提供的时区信息并在注册表中定义,但它支持创建自定义时区。 它还支持时区信息的序列化和反序列化。

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

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

另请参阅