在这里,我们研究了夏令时(DST)对本地DateTime值的影响,使用Utc DateTime值的最佳实践,使最佳实践更难遵循的原因以及应该如何指定AssumeLocal(或AssumeUniversal)和AdjustToUniversal。
作为.NET开发人员,我习惯于对.NET类库进行精心设计。
这是一把双刃剑,因为当任何应用程序中最常用的类型之一开始行为异常时,可能会让您感到惊讶。因为当DateTime ——任何应用程序中最常用的类型之一——开始表现不正常时,它可能会让您措手不及。
如果要遵循本文中的代码示例,请确保将计算机的时区设置为太平洋时间(美国&加拿大)。
任何不得不在Java中使用日期和时间的人,可能都会喜欢在.NET中使用单个DateTime类来表示Utc和Local时间,以及神秘的“未指定”时间。
不幸的是,该DateTime类型在处理Utc以外的任何时间时显示出一些非常意外且易于出错的行为。
始终使用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"));
}
以下时间戳记是太平洋时间DST更改前后30分钟。由于您离开DST时将时钟调回一小时,因此它们的本地时间均为1:30 AM。
//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时间都可以很好地工作,但在Local时间或两者的结合时都不能使用:
//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属性的值来做到这一点。“
在DST更改期间,全年大多数情况下,这些不一致仅会触发两个小时。它们很可能会被单元测试或质量检查遗漏。
最佳实践
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值,因为转换为Local和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);