在 DateTime、DateOnly、DateTimeOffset、TimeSpan、TimeOnly 和 TimeZoneInfo 之間進行選擇

.NET 應用程式可以數種方式使用日期和時間資訊。 日期和時間資訊的較常見用法包含:

  • 只反映日期,使時間資訊不重要。
  • 只反映時間,使日期資訊不重要。
  • 反映未與特定時間或位置密切相關 (例如大多數國際連鎖的商店在工作日上午 9:00 開始營業) 的抽象日期和時間。
  • 從 .NET 的外部來源中擷取日期和時間資訊,通常其中的日期和時間資訊會儲存為簡單資料類型。
  • 若要唯一且明確地識別單一時間點。 有些應用程式只在主機系統上需要明確的日期和時間。 其他應用程式則需要在不同系統上都明確 (也就是一個系統上已序列化的日期可以有意義地還原序列化,並且在世界各地的另一個系統上使用)。
  • 保留多個相關時間 (例如要求者的當地時間和 Web 要求的回條之伺服器時間)。
  • 若要執行日期和時間運算,且該運算可能有會唯一明確地識別單一時間點的結果。

.NET 包含 DateTimeDateOnlyDateTimeOffsetTimeSpanTimeOnlyTimeZoneInfo 型別,這些型別全都能用來建置使用日期和時間的應用程式。

注意

本文並不討論 TimeZone,因為其功能幾乎已完全合併入 TimeZoneInfo 類別。 可能的話,請使用 TimeZoneInfo 類別而非 TimeZone 類別。

DateTimeOffset 結構

DateTimeOffset 結構代表日期和時間值,以及表示該值與 UTC 差異大小的位移。 因此,該值一律明確地識別單一時間點。

DateTimeOffset 類型包含 DateTime 類型的所有功能並感知時區。 因此適用於執行下列內容的應用程式:

  • 唯一且明確地識別單一時間點。 DateTimeOffset 類型可用來明確定義「現在」的意義,以記錄交易的時間、記錄系統或應用程式事件的時間以及記錄檔案建立及修改的時間。
  • 執行一般日期和時間運算。
  • 只要時間已儲存為兩個不同的值或一個結構中的兩個成員,就可保留多個相關時間。

注意

DateTimeOffset 值的用途比 DateTime 值的更為普遍。 因此對於應用程式開發,應該考慮將 DateTimeOffset 做為預設的日期和時間類型。

DateTimeOffset 值與特定的時區並沒有密切關係,但可能來自各種不同的時區。 下列範例會列出可和一些 DateTimeOffset 的值有關的時區 (包括本機的太平洋標準時間)。

using System;
using System.Collections.ObjectModel;

public class TimeOffsets
{
   public static void Main()
   {
      DateTime thisDate = new DateTime(2007, 3, 10, 0, 0, 0);
      DateTime dstDate = new DateTime(2007, 6, 10, 0, 0, 0);
      DateTimeOffset thisTime;

      thisTime = new DateTimeOffset(dstDate, new TimeSpan(-7, 0, 0));
      ShowPossibleTimeZones(thisTime);

      thisTime = new DateTimeOffset(thisDate, new TimeSpan(-6, 0, 0));
      ShowPossibleTimeZones(thisTime);

      thisTime = new DateTimeOffset(thisDate, new TimeSpan(+1, 0, 0));
      ShowPossibleTimeZones(thisTime);
   }

   private static void ShowPossibleTimeZones(DateTimeOffset offsetTime)
   {
      TimeSpan offset = offsetTime.Offset;
      ReadOnlyCollection<TimeZoneInfo> timeZones;

      Console.WriteLine("{0} could belong to the following time zones:",
                        offsetTime.ToString());
      // Get all time zones defined on local system
      timeZones = TimeZoneInfo.GetSystemTimeZones();
      // Iterate time zones
      foreach (TimeZoneInfo timeZone in timeZones)
      {
         // Compare offset with offset for that date in that time zone
         if (timeZone.GetUtcOffset(offsetTime.DateTime).Equals(offset))
            Console.WriteLine("   {0}", timeZone.DisplayName);
      }
      Console.WriteLine();
   }
}
// This example displays the following output to the console:
//       6/10/2007 12:00:00 AM -07:00 could belong to the following time zones:
//          (GMT-07:00) Arizona
//          (GMT-08:00) Pacific Time (US & Canada)
//          (GMT-08:00) Tijuana, Baja California
//
//       3/10/2007 12:00:00 AM -06:00 could belong to the following time zones:
//          (GMT-06:00) Central America
//          (GMT-06:00) Central Time (US & Canada)
//          (GMT-06:00) Guadalajara, Mexico City, Monterrey - New
//          (GMT-06:00) Guadalajara, Mexico City, Monterrey - Old
//          (GMT-06:00) Saskatchewan
//
//       3/10/2007 12:00:00 AM +01:00 could belong to the following time zones:
//          (GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna
//          (GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague
//          (GMT+01:00) Brussels, Copenhagen, Madrid, Paris
//          (GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb
//          (GMT+01:00) West Central Africa
Imports System.Collections.ObjectModel

Module TimeOffsets
    Public Sub Main()
        Dim thisTime As DateTimeOffset

        thisTime = New DateTimeOffset(#06/10/2007#, New TimeSpan(-7, 0, 0))
        ShowPossibleTimeZones(thisTime)

        thisTime = New DateTimeOffset(#03/10/2007#, New TimeSpan(-6, 0, 0))
        ShowPossibleTimeZones(thisTime)

        thisTime = New DateTimeOffset(#03/10/2007#, New TimeSpan(+1, 0, 0))
        ShowPossibleTimeZones(thisTime)
    End Sub

    Private Sub ShowPossibleTimeZones(offsetTime As DateTimeOffset)
        Dim offset As TimeSpan = offsetTime.Offset
        Dim timeZones As ReadOnlyCollection(Of TimeZoneInfo)

        Console.WriteLine("{0} could belong to the following time zones:", _
                          offsetTime.ToString())
        ' Get all time zones defined on local system
        timeZones = TimeZoneInfo.GetSystemTimeZones()
        ' Iterate time zones
        For Each timeZone As TimeZoneInfo In timeZones
            ' Compare offset with offset for that date in that time zone
            If timeZone.GetUtcOffset(offsetTime.DateTime).Equals(offset) Then
                Console.WriteLine("   {0}", timeZone.DisplayName)
            End If
        Next
        Console.WriteLine()
    End Sub
End Module
' This example displays the following output to the console:
'       6/10/2007 12:00:00 AM -07:00 could belong to the following time zones:
'          (GMT-07:00) Arizona
'          (GMT-08:00) Pacific Time (US & Canada)
'          (GMT-08:00) Tijuana, Baja California
'       
'       3/10/2007 12:00:00 AM -06:00 could belong to the following time zones:
'          (GMT-06:00) Central America
'          (GMT-06:00) Central Time (US & Canada)
'          (GMT-06:00) Guadalajara, Mexico City, Monterrey - New
'          (GMT-06:00) Guadalajara, Mexico City, Monterrey - Old
'          (GMT-06:00) Saskatchewan
'       
'       3/10/2007 12:00:00 AM +01:00 could belong to the following time zones:
'          (GMT+01:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna
'          (GMT+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague
'          (GMT+01:00) Brussels, Copenhagen, Madrid, Paris
'          (GMT+01:00) Sarajevo, Skopje, Warsaw, Zagreb
'          (GMT+01:00) West Central Africa

此輸出顯示在本範例中的每個日期和時間值可屬於至少三個不同時區。 2007 年 6 月 10 日的 DateTimeOffset 值顯示出若日期和時間值代表日光節約時間,則其相對於 UTC 的位移甚至不一定會對應於起始時區的基底 UTC 位移,或對應於從其顯示名稱中找到的相對於 UTC 的位移。 因為單一 DateTimeOffset 的值並不會和時區緊密結合,所以該值無法反映與日光節約時間相互轉換的時區。 當使用日期和時間運算來管理 DateTimeOffset 值的時候,這會發生問題。 如需在考慮時區調整規則的方式下該如何執行日期和時間運算的討論,請參閱使用日期和時間執行算術運算

DateTime 結構

DateTime 值會定義特定的日期和時間。 其中包含 Kind 屬性,該屬性提供該日期和時間所屬的有限時區資訊。 由 DateTimeKind 屬性傳回的 Kind 值,表示 DateTime 值是否代表當地時間 (DateTimeKind.Local)、國際標準時間 (UTC) (DateTimeKind.Utc) 或未指定的時間 (DateTimeKind.Unspecified)。

結構 DateTime 適用於具有下列一或多個特性的應用程式:

  • 使用抽象的日期和時間。
  • 使用遺漏時區資訊的日期和時間。
  • 只使用 UTC 日期和時間。
  • 執行日期和時間運算,但關心其一般結果。 例如,在對特定日期和時間加入六個月的加法運算中,該結果是否對日光節約時間進行調整通常並不重要。

除非特定的 DateTime 值代表 UTC,否則該日期和時間值的可攜性通常是模稜兩可或受限制的。 例如,如果 DateTime 值代表當地時間,則在該當地時區是可攜式的 (也就是說,如果值在相同時區的另一個系統上還原序列化,則該值仍會明確地識別單一時間點)。 在當地時區外, DateTime 的值可有多重解譯。 如果該值的 Kind 屬性是 DateTimeKind.Unspecified,則其可攜性更差:現在其於相同的時區中模稜兩可,即使在第一次序列化的同一個系統上也可能如此。 只有當 DateTime 值代表 UTC 時,該值才會明確地識別單一時間點,無論該值所使用的系統或時區為何。

重要

儲存或共用 DateTime 資料時,請使用 UTC 並將 DateTime 值的 Kind 屬性設定為 DateTimeKind.Utc

DateOnly 結構

DateOnly 結構代表不含時間的特定日期。 因為沒有時間元件,所以該結構代表從一天開始到一天結束的日期。 此結構非常適合用來儲存特定日期,例如生日、週年日、節日或商務相關日期。

雖然您可以在忽略時間元件時使用 DateTime,但對 DateTime 使用 DateOnly 有幾個優點:

  • 如果結構是依時區時差,則 DateTime 結構可能會向前一天或下一天變換。 DateOnly 無法依時區時差,而且一律代表設定的日期。
  • 序列化 DateTime 結構包含時間元件,這可能會遮蔽資料的意圖。 此外,DateOnly 序列化的資料較少。
  • 當程式碼與資料庫 (例如 SQL Server) 互動時,整個日期通常會儲存為 date 資料類型,不包含時間。 DateOnly 更適合比對資料庫類型。

如需關於 DateOnly 的詳細資訊,請參閱如何使用 DateOnly 和 TimeOnly 結構

重要

.NET Framework 中不提供 DateOnly

TimeSpan 結構

TimeSpan 結構表示時間間隔。 其兩個一般用法為:

  • 反映出兩個日期和時間值之間的時間間隔。 例如,從另一個值中減去 DateTime 值會傳回 TimeSpan 值。
  • 測量已耗用時間。 例如,Stopwatch.Elapsed 屬性會傳回 TimeSpan 值,該值反映出在呼叫開始測量已耗用時間的 Stopwatch 方法其中一種後已耗用的時間間隔。

若在未參考一天中特定時間的情況下反映時間,則 TimeSpan 值也可用來取代 DateTime 值。 此使用方式類似於 DateTime.TimeOfDayDateTimeOffset.TimeOfDay 屬性,其會傳回 TimeSpan 值,代表沒有參考日期的時間。 例如, TimeSpan 結構可用來反映商店每日開始營業或打烊的時間,或可用來代表任何有規律事件發生的時間。

下列範例會定義 StoreInfo 結構,其中包含用來儲存開始營業和打烊時間的 TimeSpan 物件,以及代表商店所在時區的 TimeZoneInfo 物件。 該結構也包含兩種方法, IsOpenNowIsOpenAt,假定使用者處於當地時區,該結構會表示商店是否於使用者指定的時間開始營業。

using System;

public struct StoreInfo
{
   public String store;
   public TimeZoneInfo tz;
   public TimeSpan open;
   public TimeSpan close;

   public bool IsOpenNow()
   {
      return IsOpenAt(DateTime.Now.TimeOfDay);
   }

   public bool IsOpenAt(TimeSpan time)
   {
      TimeZoneInfo local = TimeZoneInfo.Local;
      TimeSpan offset = TimeZoneInfo.Local.BaseUtcOffset;

      // Is the store in the same time zone?
      if (tz.Equals(local)) {
         return time >= open & time <= close;
      }
      else {
         TimeSpan delta = TimeSpan.Zero;
         TimeSpan storeDelta = TimeSpan.Zero;

         // Is it daylight saving time in either time zone?
         if (local.IsDaylightSavingTime(DateTime.Now.Date + time))
            delta = local.GetAdjustmentRules()[local.GetAdjustmentRules().Length - 1].DaylightDelta;

         if (tz.IsDaylightSavingTime(TimeZoneInfo.ConvertTime(DateTime.Now.Date + time, local, tz)))
            storeDelta = tz.GetAdjustmentRules()[tz.GetAdjustmentRules().Length - 1].DaylightDelta;

         TimeSpan comparisonTime = time + (offset - tz.BaseUtcOffset).Negate() + (delta - storeDelta).Negate();
         return comparisonTime >= open & comparisonTime <= close;
      }
   }
}
Public Structure StoreInfo
    Dim store As String
    Dim tz As TimeZoneInfo
    Dim open As TimeSpan
    Dim close As TimeSpan

    Public Function IsOpenNow() As Boolean
        Return IsOpenAt(Date.Now.TimeOfDay)
    End Function

    Public Function IsOpenAt(time As TimeSpan) As Boolean
        Dim local As TimeZoneInfo = TimeZoneInfo.Local
        Dim offset As TimeSpan = TimeZoneInfo.Local.BaseUtcOffset

        ' Is the store in the same time zone?
        If tz.Equals(local) Then
            Return time >= open AndAlso time <= close
        Else
            Dim delta As TimeSpan = TimeSpan.Zero
            Dim storeDelta As TimeSpan = TimeSpan.Zero

            ' Is it daylight saving time in either time zone?
            If local.IsDaylightSavingTime(Date.Now.Date + time) Then
                delta = local.GetAdjustmentRules(local.GetAdjustmentRules().Length - 1).DaylightDelta
            End If
            If tz.IsDaylightSavingTime(TimeZoneInfo.ConvertTime(Date.Now.Date + time, local, tz))
                storeDelta = tz.GetAdjustmentRules(tz.GetAdjustmentRules().Length - 1).DaylightDelta
            End If
            Dim comparisonTime As TimeSpan = time + (offset - tz.BaseUtcOffset).Negate() + (delta - storeDelta).Negate
            Return (comparisonTime >= open AndAlso comparisonTime <= close)
        End If
    End Function
End Structure

然後 StoreInfo 結構可供用戶端程式碼使用,如下所示。

public class Example
{
   public static void Main()
   {
      // Instantiate a StoreInfo object.
      var store103 = new StoreInfo();
      store103.store = "Store #103";
      store103.tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
      // Store opens at 8:00.
      store103.open = new TimeSpan(8, 0, 0);
      // Store closes at 9:30.
      store103.close = new TimeSpan(21, 30, 0);

      Console.WriteLine("Store is open now at {0}: {1}",
                        DateTime.Now.TimeOfDay, store103.IsOpenNow());
      TimeSpan[] times = { new TimeSpan(8, 0, 0), new TimeSpan(21, 0, 0),
                           new TimeSpan(4, 59, 0), new TimeSpan(18, 31, 0) };
      foreach (var time in times)
         Console.WriteLine("Store is open at {0}: {1}",
                           time, store103.IsOpenAt(time));
   }
}
// The example displays the following output:
//       Store is open now at 15:29:01.6129911: True
//       Store is open at 08:00:00: True
//       Store is open at 21:00:00: False
//       Store is open at 04:59:00: False
//       Store is open at 18:31:00: False
Module Example
    Public Sub Main()
        ' Instantiate a StoreInfo object.
        Dim store103 As New StoreInfo()
        store103.store = "Store #103"
        store103.tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time")
        ' Store opens at 8:00.
        store103.open = new TimeSpan(8, 0, 0)
        ' Store closes at 9:30.
        store103.close = new TimeSpan(21, 30, 0)

        Console.WriteLine("Store is open now at {0}: {1}",
                          Date.Now.TimeOfDay, store103.IsOpenNow())
        Dim times() As TimeSpan = {New TimeSpan(8, 0, 0),
                                    New TimeSpan(21, 0, 0),
                                    New TimeSpan(4, 59, 0),
                                    New TimeSpan(18, 31, 0)}
        For Each time In times
            Console.WriteLine("Store is open at {0}: {1}",
                              time, store103.IsOpenAt(time))
        Next
    End Sub
End Module
' The example displays the following output:
'       Store is open now at 15:29:01.6129911: True
'       Store is open at 08:00:00: True
'       Store is open at 21:00:00: False
'       Store is open at 04:59:00: False
'       Store is open at 18:31:00: False

TimeOnly 結構

TimeOnly 結構代表當天時間值,例如每日鬧鐘,或您每天午餐的時間。 TimeOnly 限制為 00:00:00.0000000 - 23:59:59.9999999,即一天的特定時間。

在引進 TimeOnly 類型之前,程式設計師通常會使用 DateTime 類型或 TimeSpan 類型來代表特定時間。 不過,使用這些結構來模擬沒有日期的時間可能會造成一些問題,這些問題可使用 TimeOnly 解決:

  • TimeSpan 代表已耗用時間,例如使用停止監看式測量的時間。 上限範圍超過 29,000 年,而且其值可以是負值,表示時間往後移動。 負數 TimeSpan 不表示一天的特定時間。
  • 如果使用 TimeSpan 作為一天的時間,系統有可能會將其操作到 24 小時以外的值。 TimeOnly 就沒有此風險。 例如,如果員工的工作班次從 18:00 開始,且持續 8 小時,將 8 小時加至 TimeOnly 結構會累積為 2:00。
  • 若針對一天中的時間使用 DateTime,需要有任意日期與時間相關聯,之後再忽略。 選擇 DateTime.MinValue (0001-01-01-01) 作為日期是常見的作法,不過,如果從 DateTime 值減去小時,可能會發生例外狀況 OutOfRangeTimeOnly 因為時間在 24 小時的時間範圍內向前和向後移動,因此沒有這個問題。
  • 序列化 DateTime 結構包含日期元件,這可能會遮蔽資料的意圖。 此外,TimeOnly 序列化的資料較少。

如需關於 TimeOnly 的詳細資訊,請參閱如何使用 DateOnly 和 TimeOnly 結構

重要

.NET Framework 中不提供 TimeOnly

TimeZoneInfo 類別

TimeZoneInfo class represents any of the Earth's time zones, and enables the conversion of any date and time in one time zone to its equivalent in another time zone. TimeZoneInfo 類別讓您能夠處理日期和時間,讓任何日期和時間值能明確地識別單一時間點。 TimeZoneInfo 類別也可延伸。 雖然這取決於提供給 Windows 系統和定義於登錄中的時區資訊,但它支援建立自訂的時區。 它也支援時區資訊的序列化和還原序列化。

在某些情況下,欲充分利用 TimeZoneInfo 類別可能需要進一步的開發工作。 如果日期和時間值與所屬的時區沒有緊密結合,則需要進一步的工作。 除非您的應用程式提供讓日期和時間與相關聯時區連結的機制,否則很容易就會讓特定日期和時間值與其時區解除關聯。 連結此資訊的一種方法是定義類別或結構,其中包含日期和時間值,以及與其相關聯的時區物件。

若要利用 .NET 中的時區支援,您必須知道在具現化該日期和時間物件時,日期和時間值所屬的時區。 時區通常不為人知,特別是在 Web 或網路應用程式中。

另請參閱