.NET DateTime,关于最佳实践和时间旅行的故事





5.00/5 (11投票s)
在本文中,我们将探讨如何避免 DateTime 问题。
作为一名 .NET 开发人员,我已经习惯了 .NET 类库的良好架构。
这是一把双刃剑,因为当 DateTime(任何应用程序中最常用的类型之一)开始出现异常时,您可能会措手不及。
如果您想跟随本文中的代码示例,请确保将计算机的时区设置为太平洋时间(美国和加拿大)。
照片由 Søren Lundtoft 拍摄,标记为免费使用
任何不得不忍受在 Java 中使用日期和时间的人,可能都会欣赏在 .NET 中拥有一个单独的 DateTime
类,它代表了 Utc 时间和本地时间,以及神秘的“未指定”时间的便利性。
不幸的是,在使用除 Utc 时间之外的任何时间时,DateTime
类型会显示出一些非常意外且容易出错的行为。
始终使用 Utc
DateTime
值!
本地DateTime
值应仅限于 UI 层。
时间位移发生在凌晨 1:00 整
我将从查看夏令时 (DST) 对本地 DateTime
值的影响开始本文。
在下面的代码片段中,我将在每个 DateTime
值的完整内容作为注释显示。 您可以使用此扩展方法来实现相同的格式。
public static class DateTimeUtils
{
public static void Print(this DateTime t) =>
Console.WriteLine(t.ToString("yyyy-MM-dd HH:mm") + " " +
t.Kind + " " +
(t.IsDaylightSavingTime() ? "DST" : "ST"));
}
以下时间戳是太平洋时间夏令时更改之前和之后的 30 分钟。 因为离开夏令时时您会将时钟拨回一小时,所以它们的本地时间都是凌晨 1:30。
//1985-10-27 08:30 Utc ST
var utc1 = new DateTime(1985, 10, 27, 8, 30, 0, DateTimeKind.Utc);
//1985-10-27 09:30 Utc ST
var utc2 = new DateTime(1985, 10, 27, 9, 30, 0, DateTimeKind.Utc);
//1985-10-27 01:30 Local DST
var local1 = utc1.ToLocalTime();
//1985-10-27 01:30 Local ST
var local2 = utc2.ToLocalTime();
所以 utc1
和 utc2
相差 1 小时,local1
和 local2
也是如此,因为它们代表相同的时刻。 转换为本地时间时不会丢失任何信息:local1
和 local2
可以正确地转换回 UTC。
//1985-10-27 08:30 Utc ST
local1.ToUniversalTime();
//1985-10-27 09:30 Utc ST
local2.ToUniversalTime();
当我们开始使用运算符时,事情变得很奇怪。 等式和算术运算符对于 Utc 时间来说工作良好,但对于本地时间或两者的混合则会中断。
//Utc
Console.WriteLine(utc1 == utc2); //False
Console.WriteLine(utc2 - utc1); //01:00:00
//Local
Console.WriteLine(local1 == local2); //True (wrong!)
Console.WriteLine(local2 - local1); //00:00:00 (wrong!)
//Mix
Console.WriteLine(local1 - utc1); //-07:00:00 (wrong!)
此行为已部分记录,但仍然非常容易出错
“Equality 运算符通过比较它们的刻度数来确定两个 DateTime 值是否相等。 在比较 DateTime 对象之前,请确保这些对象表示同一时区的时间。 您可以通过比较它们的 Kind 属性的值来做到这一点。“
大多数这些不一致现象仅在一年中的两个小时内触发,即在夏令时更改期间。 它们很可能被单元测试或质量保证遗漏。
图片由 Emanhattan 制作,根据 Creative Commons 许可使用
最佳实践
DateTime
问题可以通过一个简单易行的最佳实践来避免:仅使用 Utc DateTime
值!
本地 DateTime
值的使用应仅限于 UI 层。 如果您使用的是 WPF 或其他支持数据绑定的 UI 框架,您可以考虑一直使用 Utc 到 UI,并将与本地时间的相互转换烘焙到数据绑定本身中。
在之前的帖子中,我甚至尝试过完全不使用 DateTime
,而是用一种仅允许 Utc 时间的兼容类型来替换它。
public struct UtcDateTime
{
private readonly DateTime Time;
public UtcDateTime(DateTime time)
{
switch (time.Kind)
{
case DateTimeKind.Utc:
Time = time;
break;
case DateTimeKind.Local:
Time = time.ToUniversalTime();
break;
default:
throw new NotSupportedException
("UtcDateTime cannot be initialized with an Unspecified DateTime.");
}
}
public static implicit operator UtcDateTime(DateTime t) => new UtcDateTime(t);
public static implicit operator DateTime(UtcDateTime t) => t.Time;
//Add implementation for operators and other utility methods
}
DateTime 解析会返回错误的 1985 年
(In which Biff is corrupt, and powerful...)
使我们的最佳实践更难遵循的是,即使输入字符串表示 Utc 时间,Parse 和 ParseExact 方法也倾向于返回本地时间,甚至未指定时间的 DateTime
值。
//Parsing a string can give us an Unspecified DateTime.
//1985-10-27 01:30 Unspecified ST
DateTime.Parse("1985-10-27T01:30:00", CultureInfo.InvariantCulture);
//Parsing a string will give us a Local DateTime even if we specify that
//the string represents a Utc time!
//At least the conversion is correct.
//1985-10-27 01:30 Local DST
DateTime.Parse("1985-10-27T08:30:00", CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal);
//Parsing a string will give us a Local DateTime even if the string is
//explicitly marked as Utc ("Z").
//Again, the conversion to Local is correct.
//1985-10-27 01:30 Local DST
DateTime.Parse("1985-10-27T08:30:00Z", CultureInfo.InvariantCulture);
//ParseExact may return an Unspecified value even if the string is
//explicitly marked as Utc ("Z"). This is because the format string is
//slightly off: "\\Z" instead of "Z".
//1985-10-27 08:30 Unspecified ST (wrong!)
DateTime.ParseExact("1985-10-27T08:30:00Z", "yyyy-MM-ddTHH:mm:ss\\Z",
CultureInfo.InvariantCulture);
//1985-10-27 01:30 Local DST
DateTime.ParseExact("1985-10-27T08:30:00Z", "yyyy-MM-ddTHH:mm:ssZ",
CultureInfo.InvariantCulture);
我特别不喜欢使用未指定的 DateTime
值,因为它们在转换为本地时间和 Utc 时间时行为不一致。
//1985-10-27 08:30 Unspecified ST
var t = new DateTime(1985, 10, 27, 8, 30, 0);
//Behaves like Utc when converted to Local
//1985-10-27 01:30 Local DST
t.ToLocalTime();
//Behaves like Local when converted to UTc
//1985-10-27 16:30 Utc ST
t.ToUniversalTime();
避免围绕解析 DateTime
值的所有复杂性的最佳实践是始终指定 AssumeLocal(或者,如果更适合您的用例,则为 AssumeUniversal
)和 AdjustToUniversal。 这使得解析的行为更加一致,并且始终返回准备好在应用程序中存储或使用的 Utc 值。
//Thanks to AdjustToUniversal (in conjunction with AssumeLocal or
//AssumeUniversal), returned values are consistently Utc!
//AdjustToUniversal alone is not enough to guarantee a Utc result.
//1985-10-27 01:30 Unspecified ST
DateTime.Parse("1985-10-27T01:30:00", CultureInfo.InvariantCulture,
DateTimeStyles.AdjustToUniversal);
//AssumeLocal takes care of avoiding Unspecified values being returned.
//AssumeUniversal works in a similar way.
//1985-10-27 09:30 Utc ST
DateTime.Parse("1985-10-27T01:30:00", CultureInfo.InvariantCulture,
DateTimeStyles.AssumeLocal |
DateTimeStyles.AdjustToUniversal);
//1985-10-27 08:30 Utc ST
DateTime.Parse("1985-10-27T08:30:00", CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal |
DateTimeStyles.AdjustToUniversal);
//If the string being parsed is marked as Utc (or has a specific timezone)
//AssumeLocal is ignored. This is good!
//1985-10-27 08:30 Utc ST
DateTime.Parse("1985-10-27T08:30:00Z", CultureInfo.InvariantCulture,
DateTimeStyles.AssumeLocal |
DateTimeStyles.AdjustToUniversal);
//We still have the problem with ParseExact, if the format string is
//incorrect.
//1985-10-27 16:30 Utc ST (wrong!)
DateTime.ParseExact("1985-10-27T08:30:00Z", "yyyy-MM-ddTHH:mm:ss\\Z",
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeLocal |
DateTimeStyles.AdjustToUniversal);
//Using AssumeUniversal works around the problem.
//1985-10-27 08:30 Utc ST
DateTime.ParseExact("1985-10-27T08:30:00Z", "yyyy-MM-ddTHH:mm:ss\\Z",
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal |
DateTimeStyles.AdjustToUniversal);
哇,这太夸张了!
虽然所有这些都是相当基础的东西,但包括我在内的许多经验丰富的工程师有时会被它绊倒。 影响可能非常糟糕,包括崩溃和数据丢失或损坏。
希望您发现此信息有用,并且足够可怕以记住遵循最佳实践 :-)。
感谢阅读!