DateTime
和 DateTimeOffset
有什么区别,什么时候应该使用?
目前,我们有一种以时区感知方式处理 .NET DateTime
的标准方法:每当我们生成 DateTime
时,我们都会使用 UTC(例如使用 DateTime.UtcNow
),并且每当我们显示一个 DateTime
时,我们从 UTC 转换回用户的本地时间。
这很好用,但我一直在阅读 DateTimeOffset
以及它如何在对象本身中捕获本地时间和 UTC 时间。
DateTimeOffset
表示瞬时时间(也称为绝对时间)。我的意思是每个人都通用的时间点(不考虑 leap seconds 或 time dilation 的相对论效应)。另一种表示瞬时时间的方法是使用 DateTime
,其中 .Kind
是 DateTimeKind.Utc
。
这与 日历时间(也称为 公民时间)不同,后者是某人日历上的一个位置,并且在全球范围内有许多不同的日历。我们将这些日历称为时区。日历时间由 DateTime
表示,其中 .Kind
是 DateTimeKind.Unspecified
或 DateTimeKind.Local
。并且 .Local
仅在您隐含了解使用结果的计算机所在位置的情况下才有意义。 (例如,用户的工作站)
那么,为什么是 DateTimeOffset
而不是 UTC DateTime
? 这都是关于透视的。让我们打个比方——我们将假装自己是摄影师。
想象一下,您站在日历时间线上,将相机对准摆在您面前的瞬时时间线上的一个人。您根据您的时区规则排列您的相机 - 由于夏令时或您的时区法律定义的其他更改,这些规则会定期更改。 (你的手不稳,所以你的相机会摇晃。)
站在照片中的人会看到你的相机来自的角度。如果其他人在拍照,他们可能从不同的角度。这就是 DateTimeOffset
的 Offset
部分所代表的内容。
因此,如果您将相机标记为“东部时间”,则有时您从-5 指向,有时您从-4 指向。世界各地都有摄像机,它们都标记了不同的事物,并且都从不同的角度指向同一个瞬时时间线。其中一些彼此相邻(或重叠),因此仅知道偏移量不足以确定时间与哪个时区相关。
那么UTC呢?好吧,这是保证手部稳定的唯一相机。它在三脚架上,牢固地固定在地面上。它不会去任何地方。我们将其视角称为零偏移。
https://i.stack.imgur.com/QE5xq.png
那么 - 这个类比告诉我们什么?它提供了一些直观的指导方针——
如果您要表示相对于某个特定地点的时间,请使用 DateTime 以日历时间表示。请确保您永远不会将一个日历与另一个日历混淆。未指定应该是您的假设。本地仅对来自 DateTime.Now 有用。例如,我可能会获取 DateTime.Now 并将其保存在数据库中 - 但是当我检索它时,我必须假设它是未指定的。我不能相信我的本地日历与最初的日历相同。
如果您必须始终确定时刻,请确保您代表的是瞬时时间。使用 DateTimeOffset 来强制执行它,或者按照惯例使用 UTC DateTime。
如果您需要跟踪瞬时时间,但您还想知道“用户认为这是他们当地日历上的什么时间?” - 那么你必须使用 DateTimeOffset。这对于计时系统非常重要,例如,对于技术和法律问题。
如果您需要修改以前记录的 DateTimeOffset - 您在偏移量中没有足够的信息来确保新的偏移量仍然与用户相关。您还必须存储一个时区标识符(想想 - 我需要那个相机的名称,这样即使位置发生了变化,我也可以拍一张新照片)。还应该指出的是,Noda Time 对此有一个称为 ZonedDateTime 的表示,而 .Net 基类库没有类似的东西。您需要同时存储 DateTimeOffset 和 TimeZoneInfo.Id 值。
有时,您会想要表示“查看它的人”的本地日历时间。例如,在定义今天的含义时。今天总是午夜到午夜,但这些代表了瞬时时间线上几乎无限数量的重叠范围。 (实际上,我们有有限数量的时区,但您可以将偏移量表达到刻度线)因此,在这些情况下,请确保您了解如何限制“谁在问?”质疑到单个时区,或酌情将它们转换回瞬时时间。
以下是关于 DateTimeOffset
的其他一些小细节,可以支持这个类比,以及一些保持直截了当的技巧:
如果您比较两个 DateTimeOffset 值,则在比较之前它们首先被归一化为零偏移量。换句话说,2012-01-01T00:00:00+00:00 和 2012-01-01T02:00:00+02:00 指的是同一个瞬时,因此是等价的。
如果您正在进行任何单元测试并且需要确定偏移量,请分别测试 DateTimeOffset 值和 .Offset 属性。
.Net 框架中内置了一种单向隐式转换,可让您将 DateTime 传递给任何 DateTimeOffset 参数或变量。这样做时,.Kind 很重要。如果您传递 UTC 类型,它将以零偏移量传入,但如果您传递 .Local 或 .Unspecified,它将假定为本地。该框架基本上是在说,“好吧,你让我将日历时间转换为瞬时时间,但我不知道这是从哪里来的,所以我打算使用本地日历。”如果您在具有不同时区的计算机上加载未指定的 DateTime,这是一个巨大的问题。 (恕我直言 - 这应该抛出一个异常 - 但它没有。)
无耻的插头:
许多人与我分享他们发现这个类比非常有价值,因此我将它包含在我的 Pluralsight 课程中,Date and Time Fundamentals。您将在标题为“日历时间与瞬时时间”的剪辑的第二个模块“上下文很重要”中找到相机类比的逐步演练。
来自微软:
DateTimeOffset 值的这些用途比 DateTime 值的用途更常见。因此,应将 DateTimeOffset 视为应用程序开发的默认日期和时间类型。
来源:"Choosing Between DateTime, DateTimeOffset, TimeSpan, and TimeZoneInfo",MSDN
当我们的应用程序处理特定的时间点(例如创建/更新记录的时间)时,我们几乎将 DateTimeOffset
用于所有事情。附带说明一下,我们也在 SQL Server 2008 中使用 DATETIMEOFFSET
。
当您只想处理日期、时间或一般意义上的处理时,我认为 DateTime
很有用。例如,如果您有一个闹钟希望每天早上 7 点响起,您可以使用 Unspecified
的 DateTimeKind
将其存储在 DateTime
中,因为您希望它在早上 7 点响起而不管 DST .但是,如果您想表示警报发生的历史记录,您可以使用 DateTimeOffset
。
混合使用 DateTimeOffset
和 DateTime
时要小心,尤其是在类型之间进行分配和比较时。此外,仅比较相同 DateTimeKind
的 DateTime
个实例,因为 DateTime
在比较时会忽略时区偏移。
Kind
相同,比较也可能出错。如果双方都有 DateTimeKind.Unspecified
,您并不真正知道他们来自同一时区。如果双方都是 DateTimeKind.Local
,大多数比较会很好,但您仍然可能会遇到错误,因为一方在本地时区不明确。实际上只有 DateTimeKind.Utc
比较是万无一失的,是的,通常首选 DateTimeOffset
。 (干杯!)
DateTime 只能存储两个不同的时间,本地时间和 UTC。 Kind 属性指示哪个。
DateTimeOffset 通过能够存储来自世界任何地方的本地时间对此进行了扩展。它还存储本地时间和 UTC 之间的偏移量。请注意 DateTime 无法做到这一点,除非您向您的班级添加一个额外的成员来存储该 UTC 偏移量。或者只使用 UTC。顺便说一句,这本身就是一个好主意。
有几个地方 DateTimeOffset
有意义。一种是当您处理重复事件和夏令时时。假设我想设置一个每天早上 9 点响起的闹钟。如果我使用“存储为 UTC,显示为本地时间”规则,那么当夏令时生效时,警报将在 不同 时间响起。
可能还有其他示例,但上面的示例实际上是我过去遇到的示例(这是在将 DateTimeOffset
添加到 BCL 之前 - 我当时的解决方案是将时间显式存储在本地时区,并将时区信息保存在旁边:基本上是 DateTimeOffset
在内部执行的操作)。
最重要的区别是 DateTime 不存储时区信息,而 DateTimeOffset 存储。
尽管 DateTime 区分 UTC 和 Local,但绝对没有与之关联的明确时区偏移量。如果您进行任何类型的序列化或转换,将使用服务器的时区。即使您通过添加分钟来偏移 UTC 时间来手动创建本地时间,您仍然可以在序列化步骤中获得一些信息,因为(由于 DateTime 中缺少任何明确的偏移量)它将使用服务器的时区偏移量。
例如,如果您使用 Json.Net 和 ISO 日期格式序列化 Kind=Local 的 DateTime 值,您将获得类似 2015-08-05T07:00:00-04
的字符串。请注意,最后一部分(-04)与您的 DateTime 或您用来计算它的任何偏移量无关......它纯粹是服务器的时区偏移量。
同时, DateTimeOffset 明确包含偏移量。它可能不包含时区的名称,但至少包含偏移量,如果您对其进行序列化,您将在值中获得显式包含的偏移量,而不是服务器的本地时间。
The most important distinction is that DateTime does not store time zone information, while DateTimeOffset does.
Microsoft 中的这段代码解释了一切:
// Find difference between Date.Now and Date.UtcNow
date1 = DateTime.Now;
date2 = DateTime.UtcNow;
difference = date1 - date2;
Console.WriteLine("{0} - {1} = {2}", date1, date2, difference);
// Find difference between Now and UtcNow using DateTimeOffset
dateOffset1 = DateTimeOffset.Now;
dateOffset2 = DateTimeOffset.UtcNow;
difference = dateOffset1 - dateOffset2;
Console.WriteLine("{0} - {1} = {2}",
dateOffset1, dateOffset2, difference);
// If run in the Pacific Standard time zone on 4/2/2007, the example
// displays the following output to the console:
// 4/2/2007 7:23:57 PM - 4/3/2007 2:23:57 AM = -07:00:00
// 4/2/2007 7:23:57 PM -07:00 - 4/3/2007 2:23:57 AM +00:00 = 00:00:00
DateTimeOffset.Now
因为您可以比较 CreatedDate
而与他们的时区无关。
TLDR,如果您不想阅读所有这些出色的答案 :-)
显式:
使用 DateTimeOffset
因为时区被强制为 UTC+0。
隐式:
使用DateTime
,您希望每个人都遵守时区始终为 UTC+0 的不成文规则。
(开发人员的旁注:显式总是比隐式好!)
(Java 开发人员的旁注,C# DateTimeOffset
== Java OffsetDateTime
,请阅读:https://www.baeldung.com/java-zoneddatetime-offsetdatetime)
主要区别在于 DateTimeOffset
可以与 TimeZoneInfo
结合使用以转换为当前时区以外的本地时间。
这在由不同时区的用户访问的服务器应用程序(例如 ASP.NET)上很有用。
我看到的 DateTimeOffset 唯一不利的一面是微软“忘记”(按设计)在他们的 XmlSerializer 类中支持它。但它已被添加到 XmlConvert 实用程序类中。
我说继续使用 DateTimeOffset 和 TimeZoneInfo 因为所有好处,只是在创建将或可能序列化到 XML 或从 XML(然后是所有业务对象)序列化的实体时要小心。
DateTime.Now
12 月 21 日星期五 18:40:11
DateTimeOffset.Now
12 月 21 日星期五 18:40:11 +02:00
因此,DateTimeOffset
存储有关时间如何与 UTC(基本上是时区)相关的信息。
DateTimeOffset
,那么您应该将它保存到 SQL Server 中的DATETIMEOFFSET
。DATETIME2
或仅DATETIME
(取决于所需的范围)适用于常规DateTime
值。是的 - 您可以从任何一对时区 + dto 或 utc 解析本地时间。不同之处在于——你总是想为每个解析计算规则,还是想预先计算它们?在许多情况下(有时出于法律考虑),DTO 是更好的选择。DateTimeOffset.Now
,你确实会得到服务器的偏移量。关键是DateTimeOffset
类型可以保留该偏移量。您可以在客户端上轻松执行此操作,将其发送到服务器,然后您的服务器将知道客户端的偏移量。