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

 

在这里,我们研究了夏令时(DST)对本地DateTime值的影响,使用Utc DateTime值的最佳实践,使最佳实践更难遵循的原因以及应该如何指定AssumeLocal(或AssumeUniversal)和AdjustToUniversal

作为.NET开发人员,我习惯于对.NET类库进行精心设计。

这是一把双刃剑,因为当任何应用程序中最常用的类型之一开始行为异常时,可能会让您感到惊讶。因为当DateTime ——任何应用程序中最常用的类型之一——开始表现不正常时,它可能会让您措手不及。

如果要遵循本文中的代码示例,请确保将计算机的时区设置为太平洋时间(美国&加拿大)。

任何不得不在Java中使用日期和时间的人,可能都会喜欢在.NET中使用单个DateTime类来表示UtcLocal时间,以及神秘的未指定时间。

不幸的是,该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();

因此utc1utc2相隔1小时对于local1local2也是一样的。因为它们表示相同的时刻。在转换为本地时间时不会丢失任何信息:local1local2可以正确地转换回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时间,ParseParseExact方法也倾向于返回本地甚至未指定的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值,因为转换为LocalUtc时的行为不一致。

//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);

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值