在各时区之间转换时间

对任何使用日期和时间的应用程序而言,处理各时区之间的差异变得愈发重要。 应用程序不再能做出如下假设:所有时间均可表示为 DateTime 结构提供的本地时间。 例如,显示美国东部当前时间的网页对东亚客户而言缺乏可信度。 本文阐述如何将时间从一个时区转换到另一个时区,以及如何转换时区感知有限的 DateTimeOffset 值。

转换为协调世界时

协调世界时 (UTC) 是一项高精度的原子时标准。 世界的时区表示为相对于 UTC 的正/负偏移量。 因此,UTC 提供一种无时区或中间时区的时间。 如果日期和时间在计算机之间的可移植性非常重要,则建议使用 UTC。 有关详细信息以及使用日期和时间的其他最佳做法,请参阅在 .NET Framework 中使用 DateTime 来编码最佳做法。 通过将各个时区转换为 UTC,可以简化时间的比较。

注意

还可以序列化 DateTimeOffset 结构,以明确表示一个时间点。 由于 DateTimeOffset 对象会存储日期和时间值以及其相对于 UTC 的偏移量,它们始终表示与 UTC 相关的某一特定时间点。

将时间转换为 UTC 的最简单的方式是调用 static(在 Visual Basic 中为 SharedTimeZoneInfo.ConvertTimeToUtc(DateTime) 方法。 该方法执行的具体转换取决于 dateTime 参数的 Kind 属性值,如下表所示:

DateTime.Kind 转换
DateTimeKind.Local 将本地时间转换为 UTC。
DateTimeKind.Unspecified 假定 dateTime 参数为本地时间并将本地时间转换为 UTC。
DateTimeKind.Utc 返回未更改的 dateTime 参数。

以下代码可将当前本地时间转换为 UTC,并将结果显示在控制台上:

DateTime dateNow = DateTime.Now;
Console.WriteLine($"The date and time are {TimeZoneInfo.ConvertTimeToUtc(dateNow)} UTC.");
Dim dateNow As Date = Date.Now
Console.WriteLine("The date and time are {0} UTC.", _
                  TimeZoneInfo.ConvertTimeToUtc(dateNow))

如果日期和时间值不表示本地时间或 UTC,ToUniversalTime 方法可能返回错误的结果。 但是,你可以使用 TimeZoneInfo.ConvertTimeToUtc 方法转换指定时区的日期和时间。 有关检索表示目标时区的 TimeZoneInfo 对象的详细信息,请参阅查找本地系统上定义的时区。 以下代码使用 TimeZoneInfo.ConvertTimeToUtc 方法将东部标准时间转换为 UTC:

DateTime easternTime = new DateTime(2007, 01, 02, 12, 16, 00);
string easternZoneId = "Eastern Standard Time";
try
{
    TimeZoneInfo easternZone = TimeZoneInfo.FindSystemTimeZoneById(easternZoneId);
    Console.WriteLine($"The date and time are {TimeZoneInfo.ConvertTimeToUtc(easternTime, easternZone)} UTC.");
}
catch (TimeZoneNotFoundException)
{
    Console.WriteLine($"Unable to find the {easternZoneId} zone in the registry.");
}
catch (InvalidTimeZoneException)
{
    Console.WriteLine($"Registry data on the {easternZoneId} zone has been corrupted.");
}
Dim easternTime As New Date(2007, 01, 02, 12, 16, 00)
Dim easternZoneId As String = "Eastern Standard Time"
Try
    Dim easternZone As TimeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(easternZoneId)
    Console.WriteLine("The date and time are {0} UTC.", _
                      TimeZoneInfo.ConvertTimeToUtc(easternTime, easternZone))
Catch e As TimeZoneNotFoundException
    Console.WriteLine("Unable to find the {0} zone in the registry.", _
                      easternZoneId)
Catch e As InvalidTimeZoneException
    Console.WriteLine("Registry data on the {0} zone has been corrupted.", _
                      easternZoneId)
End Try

如果 DateTime 对象的 Kind 属性与时区不匹配,TimeZoneInfo.ConvertTimeToUtc 方法将引发 ArgumentException。 如果 Kind 属性为 DateTimeKind.Local,但 TimeZoneInfo 对象不表示本地时区(或如果 Kind 属性为 DateTimeKind.Utc,但 TimeZoneInfo 对象不等于 TimeZoneInfo.Utc),则会出现不匹配。

所有这些方法均采用 DateTime 值作为参数,并返回 DateTime 值。 对于 DateTimeOffset 值,DateTimeOffset 结构具有 ToUniversalTime 实例方法,该方法可将当前实例的日期和时间转换为 UTC。 以下示例调用 ToUniversalTime 方法,将本地时间和几个其他时间转换为 UTC:

DateTimeOffset localTime, otherTime, universalTime;

// Define local time in local time zone
localTime = new DateTimeOffset(new DateTime(2007, 6, 15, 12, 0, 0));
Console.WriteLine("Local time: {0}", localTime);
Console.WriteLine();

// Convert local time to offset 0 and assign to otherTime
otherTime = localTime.ToOffset(TimeSpan.Zero);
Console.WriteLine("Other time: {0}", otherTime);
Console.WriteLine("{0} = {1}: {2}",
                  localTime, otherTime,
                  localTime.Equals(otherTime));
Console.WriteLine("{0} exactly equals {1}: {2}",
                  localTime, otherTime,
                  localTime.EqualsExact(otherTime));
Console.WriteLine();

// Convert other time to UTC
universalTime = localTime.ToUniversalTime();
Console.WriteLine("Universal time: {0}", universalTime);
Console.WriteLine("{0} = {1}: {2}",
                  otherTime, universalTime,
                  universalTime.Equals(otherTime));
Console.WriteLine("{0} exactly equals {1}: {2}",
                  otherTime, universalTime,
                  universalTime.EqualsExact(otherTime));
Console.WriteLine();
// The example produces the following output to the console:
//    Local time: 6/15/2007 12:00:00 PM -07:00
//
//    Other time: 6/15/2007 7:00:00 PM +00:00
//    6/15/2007 12:00:00 PM -07:00 = 6/15/2007 7:00:00 PM +00:00: True
//    6/15/2007 12:00:00 PM -07:00 exactly equals 6/15/2007 7:00:00 PM +00:00: False
//
//    Universal time: 6/15/2007 7:00:00 PM +00:00
//    6/15/2007 7:00:00 PM +00:00 = 6/15/2007 7:00:00 PM +00:00: True
//    6/15/2007 7:00:00 PM +00:00 exactly equals 6/15/2007 7:00:00 PM +00:00: True
Dim localTime, otherTime, universalTime As DateTimeOffset

' Define local time in local time zone
localTime = New DateTimeOffset(#6/15/2007 12:00:00PM#)
Console.WriteLine("Local time: {0}", localTime)
Console.WriteLine()

' Convert local time to offset 0 and assign to otherTime
otherTime = localTime.ToOffset(TimeSpan.Zero)
Console.WriteLine("Other time: {0}", otherTime)
Console.WriteLine("{0} = {1}: {2}", _
                  localTime, otherTime, _
                  localTime.Equals(otherTime))
Console.WriteLine("{0} exactly equals {1}: {2}", _
                  localTime, otherTime, _
                  localTime.EqualsExact(otherTime))
Console.WriteLine()

' Convert other time to UTC
universalTime = localTime.ToUniversalTime()
Console.WriteLine("Universal time: {0}", universalTime)
Console.WriteLine("{0} = {1}: {2}", _
                  otherTime, universalTime, _
                  universalTime.Equals(otherTime))
Console.WriteLine("{0} exactly equals {1}: {2}", _
                  otherTime, universalTime, _
                  universalTime.EqualsExact(otherTime))
Console.WriteLine()
' The example produces the following output to the console:
'    Local time: 6/15/2007 12:00:00 PM -07:00
'    
'    Other time: 6/15/2007 7:00:00 PM +00:00
'    6/15/2007 12:00:00 PM -07:00 = 6/15/2007 7:00:00 PM +00:00: True
'    6/15/2007 12:00:00 PM -07:00 exactly equals 6/15/2007 7:00:00 PM +00:00: False
'    
'    Universal time: 6/15/2007 7:00:00 PM +00:00
'    6/15/2007 7:00:00 PM +00:00 = 6/15/2007 7:00:00 PM +00:00: True
'    6/15/2007 7:00:00 PM +00:00 exactly equals 6/15/2007 7:00:00 PM +00:00: True   

将 UTC 转换为指定的时区

若要将 UTC 转换为本地时间,请参阅下方将 UTC 转换为本地时间一节。 若要将 UTC 转换为任何指定时区中的时间,请调用 ConvertTimeFromUtc 方法。 该方法采用以下两种参数:

  • 要转换的 UTC。 这必须是一个 DateTime 值,该值的 Kind 属性设置为 UnspecifiedUtc

  • UTC 要转换的目标时区。

以下代码可将 UTC 转换为中部标准时间:

DateTime timeUtc = DateTime.UtcNow;
try
{
    TimeZoneInfo cstZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time");
    DateTime cstTime = TimeZoneInfo.ConvertTimeFromUtc(timeUtc, cstZone);
    Console.WriteLine("The date and time are {0} {1}.",
                      cstTime,
                      cstZone.IsDaylightSavingTime(cstTime) ?
                              cstZone.DaylightName : cstZone.StandardName);
}
catch (TimeZoneNotFoundException)
{
    Console.WriteLine("The registry does not define the Central Standard Time zone.");
}
catch (InvalidTimeZoneException)
{
    Console.WriteLine("Registry data on the Central Standard Time zone has been corrupted.");
}
Dim timeUtc As Date = Date.UtcNow
Try
    Dim cstZone As TimeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time")
    Dim cstTime As Date = TimeZoneInfo.ConvertTimeFromUtc(timeUtc, cstZone)
    Console.WriteLine("The date and time are {0} {1}.", _
                      cstTime, _
                      IIf(cstZone.IsDaylightSavingTime(cstTime), _
                          cstZone.DaylightName, cstZone.StandardName))
Catch e As TimeZoneNotFoundException
    Console.WriteLine("The registry does not define the Central Standard Time zone.")
Catch e As InvalidTimeZoneException
    Console.WriteLine("Registry data on the Central Standard Time zone has been corrupted.")
End Try

将 UTC 转换为本地时间

若要将 UTC 转换为本地时间,请调用要转换其时间的 DateTime 对象的 ToLocalTime 方法。 该方法的具体行为取决于该对象的 Kind 属性值,如下表所示:

DateTime.Kind 转换
DateTimeKind.Local 返回未更改的 DateTime 值。
DateTimeKind.Unspecified 假定 DateTime 值为 UTC 并将 UTC 转换为本地时间。
DateTimeKind.Utc DateTime 值转换为本地时间。

注意

TimeZone.ToLocalTime 方法的行为与 DateTime.ToLocalTime 方法完全相同。 它使用一个参数,即要转换的日期和时间值。

你还可以通过使用 static(Visual Basic 中为 SharedTimeZoneInfo.ConvertTime 方法,将任何指定时区中的时间转换为本地时间。 下一节将讨论此技巧。

在任意两个时区之间转换

你可以使用 TimeZoneInfo 类的以下两个 static(Visual Basic 中为 Shared)方法之一,在任意两个时区之间进行转换:

  • ConvertTime

    此方法的参数是要转换的日期和时间值、一个 TimeZoneInfo 对象(表示日期和时间值的时区)和一个 TimeZoneInfo 对象(表示日期和时间值要转换到的目标时区)。

  • ConvertTimeBySystemTimeZoneId

    此方法的参数是要转换的日期和时间值、该值的时区的标识符以及该值要转换到的时区的标识符。

两个方法都要求要转换的日期和时间值的 Kind 属性、TimeZoneInfo 对象或表示其时区的时区标识符彼此对应。 否则会引发 ArgumentException。 例如,如果日期和时间值的 Kind 属性为 DateTimeKind.Local,而作为参数传递给该方法的 TimeZoneInfo 对象不等于 TimeZoneInfo.Local,则会引发异常。 如果作为参数传递给该方法的标识符不等于 TimeZoneInfo.Local.Id,也会引发异常。

下面的示例使用 ConvertTime 方法将夏威夷标准时间转换为本地时间:

DateTime hwTime = new DateTime(2007, 02, 01, 08, 00, 00);
try
{
    TimeZoneInfo hwZone = TimeZoneInfo.FindSystemTimeZoneById("Hawaiian Standard Time");
    Console.WriteLine("{0} {1} is {2} local time.",
            hwTime,
            hwZone.IsDaylightSavingTime(hwTime) ? hwZone.DaylightName : hwZone.StandardName,
            TimeZoneInfo.ConvertTime(hwTime, hwZone, TimeZoneInfo.Local));
}
catch (TimeZoneNotFoundException)
{
    Console.WriteLine("The registry does not define the Hawaiian Standard Time zone.");
}
catch (InvalidTimeZoneException)
{
    Console.WriteLine("Registry data on the Hawaiian Standard Time zone has been corrupted.");
}
Dim hwTime As Date = #2/01/2007 8:00:00 AM#
Try
    Dim hwZone As TimeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Hawaiian Standard Time")
    Console.WriteLine("{0} {1} is {2} local time.", _
                      hwTime, _
                      IIf(hwZone.IsDaylightSavingTime(hwTime), hwZone.DaylightName, hwZone.StandardName), _
                      TimeZoneInfo.ConvertTime(hwTime, hwZone, TimeZoneInfo.Local))
Catch e As TimeZoneNotFoundException
    Console.WriteLine("The registry does not define the Hawaiian Standard Time zone.")
Catch e As InvalidTimeZoneException
    Console.WriteLine("Registry data on the Hawaiian Standard Time zone has been corrupted.")
End Try

转换 DateTimeOffset 值

DateTimeOffset 对象表示的日期和时间值不具备完全时区感知能力,因为实例化该对象时已解除其与其时区的关联。 但是,大多数情况下,应用程序仅需根据两种相对于 UTC 的偏移量,而不是特定时区的时间来转换日期和时间。 若要执行此转换,可以调用当前实例的 ToOffset 方法。 该方法的单个参数是该方法将返回的新日期和时间值的偏移量。

例如,如果用户所请求网页的日期和时间已知,并且已被序列化为 MM/dd/yyyy hh:mm:ss zzzz 格式的字符串,以下 ReturnTimeOnServer 方法会将此日期和时间值转换为 Web 服务器上的日期和时间:

public DateTimeOffset ReturnTimeOnServer(string clientString)
{
   string format = @"M/d/yyyy H:m:s zzz";
   TimeSpan serverOffset = TimeZoneInfo.Local.GetUtcOffset(DateTimeOffset.Now);

   try
   {
      DateTimeOffset clientTime = DateTimeOffset.ParseExact(clientString, format, CultureInfo.InvariantCulture);
      DateTimeOffset serverTime = clientTime.ToOffset(serverOffset);
      return serverTime;
   }
   catch (FormatException)
   {
      return DateTimeOffset.MinValue;
   }
}
Public Function ReturnTimeOnServer(clientString As String) As DateTimeOffset
    Dim format As String = "M/d/yyyy H:m:s zzz"
    Dim serverOffset As TimeSpan = TimeZoneInfo.Local.GetUtcOffset(DateTimeOffset.Now)

    Try
        Dim clientTime As DateTimeOffset = DateTimeOffset.ParseExact(clientString, format, CultureInfo.InvariantCulture)
        Dim serverTime As DateTimeOffset = clientTime.ToOffset(serverOffset)
        Return serverTime
    Catch e As FormatException
        Return DateTimeOffset.MinValue
    End Try
End Function

如果该方法传递字符串“9/1/2007 5:32:07 -05:00”(表示比 UTC 早五个小时的时区中的日期和时间),则对于处在美国太平洋标准时区中的服务器,它将返回“9/1/2007 3:32:07 AM -07:00”。

TimeZoneInfo 类还包括通过 ToOffset(TimeSpan) 值执行时区转换的 TimeZoneInfo.ConvertTime(DateTimeOffset, TimeZoneInfo) 方法的重载。 该方法有两个参数:一个是 DateTimeOffset 值,另一个是对时间要转换到的目标时区的引用。 该方法调用返回一个 DateTimeOffset 值。 例如,可像下面那样重写上一个示例中的 ReturnTimeOnServer 方法来调用 ConvertTime(DateTimeOffset, TimeZoneInfo) 方法。

public DateTimeOffset ReturnTimeOnServer(string clientString)
{
   string format = @"M/d/yyyy H:m:s zzz";

   try
   {
      DateTimeOffset clientTime = DateTimeOffset.ParseExact(clientString, format,
                                  CultureInfo.InvariantCulture);
      DateTimeOffset serverTime = TimeZoneInfo.ConvertTime(clientTime,
                                  TimeZoneInfo.Local);
      return serverTime;
   }
   catch (FormatException)
   {
      return DateTimeOffset.MinValue;
   }
}
Public Function ReturnTimeOnServer(clientString As String) As DateTimeOffset
    Dim format As String = "M/d/yyyy H:m:s zzz"

    Try
        Dim clientTime As DateTimeOffset = DateTimeOffset.ParseExact(clientString, format, CultureInfo.InvariantCulture)
        Dim serverTime As DateTimeOffset = TimeZoneInfo.ConvertTime(clientTime, TimeZoneInfo.Local)
        Return serverTime
    Catch e As FormatException
        Return DateTimeOffset.MinValue
    End Try
End Function

另请参阅