System.DateTime詳解

作者: Artech  來源: 博客園  發布時間: 2010-09-05 13:22  閱讀: 16539 次  推薦: 3   原文鏈接   [收藏]  
摘要:對于一個分布式的應用來說,倘若客戶端和服務端部署與不同的地區,在對時間進行處理的時候,就需要考慮時區的問題。

  最近一直在負責公司內部框架的升級工作,今天對一個小問題進行了重新思考——時間的處理。具體來說,是如何有效地進行時間的處理以提供對跨時區的支持。對于一個分布式的應用來說,倘若客戶端和服務端部署與不同的地區,在對時間進行處理的時候,就需要考慮時區的問題。以我們現在的一個項目為例,這是一個為澳大利亞某機構開發的一個基于Smart Client應用(Windows Form客戶端),服務器部署于墨爾本,應用的最終用戶可能需要跨越不同的州。澳洲地廣人稀,不同的州也有可能會跨越不同的時區。假設數據庫并不支持對時區的區分,服務端需要對針對客戶端所在的時區對時間進行相應的處理。不過,對該問題解決方案的介紹我會放在后續的文章中,在這里我們先來介紹一些基礎性的內容——談談我們熟悉的System.DateTime類型。

  一、你是否知道System.DateTimeKind?

  System.DateTime類型,我們再熟悉不過。順便說一下,這個類型不是class,而是一個struct,換言之它是值類型,而不是引用類型。DateTime處理包含我們熟悉的年、月、日、時、分、秒和毫秒等基本屬性之外,還具有一個重要的表示時間類型(Kind)的屬性:Kind。該屬性的類型為System.DateTimeKind枚舉。DateTimeKind定義如下,它具有三個枚舉值:Unspecified、Utc和Local。后兩個分別表示UTC(格林威治時間)和本地時間。Unspecified顧名思義,就是尚未指定具體類型,這是默認值。

   1: [Serializable, ComVisible(true)]
   2: public enum DateTimeKind
   3: {
   4:     Unspecified,
   5:     Utc,
   6:     Local
   7: }

  在DateTime類型中,表示時間類型的Kind屬性是只讀的,只能在構造函數中指定。相關構造函數和Kind屬性的定義如下面的代碼片斷所示:

   1: [Serializable]
   2: public struct DateTime
   3: {
   4:     //Others...
   5:     public DateTimeKind Kind { get; }
   6:  
   7:     public DateTime(int year, int month, int day, int hour, int minute, int second, DateTimeKind kind);
   8:     public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, DateTimeKind kind);
   9:     public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, Calendar calendar, DateTimeKind kind);
  10: }

  雖然,Kind屬性是只讀的,但是我們還用另外一中設定Kind的方式,那就是調用DateTime的靜態方法的SpecifyKind。該方法不會真正去修改一個現有DateTime對象的Kind屬性,而是會重新創建一個新的DateTime對象。方法返回的對象具有和指定時間相同的基本屬性(年、月、日、時、分、秒和毫秒),該DateTime對象具有你指定的DateTimeKind值。

   1: public struct DateTime
   2: {
   3:     //Others...
   4:     public static DateTime SpecifyKind(DateTime value, DateTimeKind kind);
   5: }

  二、幾個常用DateTime對象的DateTimeKind

  處理直接通過構造函數構建DateTime對象之外,我們還經常用到DateTime的幾個靜態只讀屬性去獲取一些特殊的時間,比如Now、UtcNow、MinValue和MaxValue等,那么這些DateTime對象的DateTimeKind又是什么呢?

  • 當我們通過構造函數創建一個DateTime對象的時候,Kind默認為DateTimeKind.Unspecified。
  • DateTime.Now表示當前系統時間,Kind屬性值為DateTimeKind.Local,所以DateTime.Now應該是DateTime.LocalNow;
  • 而DateTime.UtcNow返回以UTC表示的當前時間,毫無疑問,Kind屬性自然是DateTimeKind.Utc;
  • DateTime.MinValue和DateTime.MaxValue表示的DateTime所能表示的最大范圍,它們的Kind屬性為DateTimeKind.Unspecified。

  上面列表對幾個常用DateTime對象Kind屬性的描述可以通過下面的程序來證實:

   1: DateTime endOfTheWorld = new DateTime(2012, 12, 21);
   2: Console.WriteLine("endOfTheWorld.Kind = {0}", endOfTheWorld.Kind);
   3: Console.WriteLine("DateTime.SpecifyKind(endOfTheWorld, DateTimeKind.Utc).Kind = {0}", 
   4:     DateTime.SpecifyKind(endOfTheWorld, DateTimeKind.Utc).Kind);
   5: Console.WriteLine("endOfTheWorld.Kind = {0}", endOfTheWorld.Kind);
   6: Console.WriteLine("DateTime.Now.Kind = {0}", DateTime.Now.Kind);
   7: Console.WriteLine("DateTime.UtcNow.Kind = {0}", DateTime.UtcNow.Kind);
   8: Console.WriteLine("DateTime.MinValue.Kind = {0}", DateTime.MinValue.Kind);
   9: Console.WriteLine("DateTime.MaxValue.Kind = {0}", DateTime.MaxValue.Kind);

  輸出結果:

   1: endOfTheWorld.Kind = Unspecified
   2: DateTime.SpecifyKind(endOfTheWorld, Dat
   3: endOfTheWorld.Kind = Unspecified
   4: DateTime.Now.Kind = Local
   5: DateTime.UtcNow.Kind = Utc
   6: DateTime.MinValue.Kind = Unspecified
   7: DateTime.MaxValue.Kind = Unspecified

  三、DateTime的對等性問題

  接下來,我們來談談另外一個比較有意思的問題——兩個DateTime對象對等性。在這之前,我首先提出這樣一個問題:“如果兩個DateTime對象相等,是否意味著它們表示同一個時間點?”我想有人會認為是。但是答案是“不一定”,我們可以舉一個反例。在下面的程序中,我創建了三個DateTime對象,年、月、日、時、分、秒均是相同的,但Kind分分別指定為DateTimeKind.Local、DateTimeKind.Unspecified和DateTimeKind.Utc。

   1: DateTime endOfTheWorld1 = new DateTime(2012, 12, 21, 0, 0, 0, DateTimeKind.Local);
   2: DateTime endOfTheWorld2 = new DateTime(2012, 12, 21, 0, 0, 0, DateTimeKind.Unspecified);
   3: DateTime endOfTheWorld3 = new DateTime(2012, 12, 21, 0, 0, 0, DateTimeKind.Utc);
   4:  
   5: Console.WriteLine("endOfTheWorld1 == endOfTheWorld2 = {0}", endOfTheWorld1 == endOfTheWorld2);
   6: Console.WriteLine("endOfTheWorld2 == endOfTheWorld3 = {0}", endOfTheWorld2 == endOfTheWorld3);

  由于我們處于東8區,基于DateTimeKind.Local的endOfTheWorld1和基于DateTimeKind.Utc的endOfTheWorld3,不可能表示的是同一個時刻。但是從下面的輸出結果來看,它們卻是“相等的”,不但如此,Kind為Unspecified的endOfTheWorld2也和這兩個時間對象相等。

   1: endOfTheWorld1 == endOfTheWorld2 = True
   2: endOfTheWorld2 == endOfTheWorld3 = True

  由此可見,DateTimeKind對等性判斷和DateTimeKind無關,那么在內部是如何進行判斷的呢?要回答這個問題,這就要談談DateTime另外一個重要的屬性——Ticks了。該屬性定義如下,是DateTime的只讀屬性,類型為長整型,表示該DateTime對象通過日期和時間體現出來的計時周期數。每個計時周期表示一百納秒,即一千萬分之一秒。1 毫秒內有 10,000 個計時周期。此屬性的值表示自公元元年( 0001 年) 1 月 1 日午夜 12:00:00(表示 DateTime.MinValue)以來經過的以100 納秒為間隔的間隔數。

   1: public struct DateTime
   2: {
   3:     //Others...
   4:     public long Ticks { get; }
   5: }

  注意,這里的基準時間0001 年 1 月 1 日午夜 12:00:00,并沒有說是一定是UTC時間,所以Ticks和DateTimeKind無關,這里通過下面的實例看出來:

   1: DateTime endOfTheWorld1 = new DateTime(2012, 12, 21, 0, 0, 0, DateTimeKind.Local);
   2: DateTime endOfTheWorld2 = new DateTime(2012, 12, 21, 0, 0, 0, DateTimeKind.Unspecified);
   3: DateTime endOfTheWorld3 = new DateTime(2012, 12, 21, 0, 0, 0, DateTimeKind.Utc);
   4:  
   5: Console.WriteLine("endOfTheWorld1.Ticks = {0}", endOfTheWorld1.Ticks);
   6: Console.WriteLine("endOfTheWorld2.Ticks = {0}", endOfTheWorld2.Ticks);
   7: Console.WriteLine("endOfTheWorld3.Ticks = {0}", endOfTheWorld3.Ticks);

  從下面的輸出結果我們不難看出,上面創建的具有不同DateTimeKind的三個DateTime的Ticks屬性的值都是相等的。實際上,DateTime的對等性判斷就是通過Ticks的大小來判斷的。

   1: endOfTheWorld1.Ticks = 634917312000000000
   2: endOfTheWorld2.Ticks = 634917312000000000
   3: endOfTheWorld3.Ticks = 634917312000000000

  我們經常說的UTC時間和本地時間之間的相互轉化,實際上指的就是將一個具有某種DateTimeKind的DateTime對象轉化成具有另外一種DateTimeKind的DateTime對象,并且確保兩個DateTime對象對象表示相同的時間點。關于時間轉換的實現,我們有很多不同的選擇。

  四、通過DateTime類型的ToLocalTime和ToUniversalTime方法實現UTC和Local的轉換

  對基于三種不同DateTimeKind的DateTime對象之間的轉化,最方便的就是直接采用DateTime類型的兩個對應的方法:ToLocalTime和ToUniversalTime,這兩個方法的定義如下。

   1: public struct DateTime
   2: {
   3:     //Others...
   4:     public DateTime ToLocalTime();
   5:     public DateTime ToUniversalTime();
   6: }

  實際上我們所說的不同DateTimeKind之間的DateTime之間的轉化主要包括兩個方面:將一個DateTimeKind.Local(或者DateTimeKind.Unspecified)時間轉換成DateTimeKind.Utc時間,或者將DateTimeKind.Utc(或者DateTimeKind.Unspecifed時間)轉換成DateTimeKind.Local時間。為了深刻地理解兩種不同轉換采用的轉化規則,我寫了如下一段程序:

   1: DateTime endOfTheWorld1 = new DateTime(2012, 12, 21, 0, 0, 0, DateTimeKind.Local);
   2: DateTime endOfTheWorld2 = new DateTime(2012, 12, 21, 0, 0, 0, DateTimeKind.Unspecified);
   3: DateTime endOfTheWorld3 = new DateTime(2012, 12, 21, 0, 0, 0, DateTimeKind.Utc);
   4:  
   5: Console.WriteLine("endOfTheWorld1.ToLocalTime() = {0}",endOfTheWorld1.ToLocalTime());
   6: Console.WriteLine("endOfTheWorld2.ToLocalTime() = {0}", endOfTheWorld2.ToLocalTime());
   7: Console.WriteLine("endOfTheWorld3.ToLocalTime() = {0}\n", endOfTheWorld3.ToLocalTime());
   8:  
   9: Console.WriteLine("endOfTheWorld1.ToUniversalTime() = {0}", endOfTheWorld1.ToUniversalTime());
  10: Console.WriteLine("endOfTheWorld2.ToUniversalTime() = {0}", endOfTheWorld2.ToUniversalTime());
  11: Console.WriteLine("endOfTheWorld3.ToUniversalTime() = {0}", endOfTheWorld3.ToUniversalTime());

  對于DataTimeKind為Utc和Local之間的轉化,沒有什么可以說得,就是一個基于時差的換算而已。大家容易忽視的是DataTimeKind.Unspecifed時間分別向其他兩種DateTimeKind時間的轉換問題。從下面的輸出我們可以看出,當DateTimeKind.Unspecifed時間向DateTimeKind.Local轉換的時候,實際上是當成DateTimeKind.Utc時間;而向DateTimeKind.Utc轉換的時候,則當成是DateTimeKind.Local。順便補充一下:不論被轉換的時間屬于怎么的DateTimeKind,調用ToLocalTime和ToUniversalTime方法的返回的時間的Kind屬性總是DateTimeKind.Local和DateTimeKind.Utc,兩者之間的轉換并不只是年月日和時分秒的改變。

   1: endOfTheWorld1.ToLocalTime() = 12/21/2012 12:00:00 AM
   2: endOfTheWorld2.ToLocalTime() = 12/21/2012 8:00:00 AM
   3: endOfTheWorld3.ToLocalTime() = 12/21/2012 8:00:00 AM
   4:  
   5: endOfTheWorld1.ToUniversalTime() = 12/21/2012 4:00:00 PM
   6: endOfTheWorld2.ToUniversalTime() = 12/21/2012 4:00:00 PM
   7: endOfTheWorld3.ToUniversalTime() = 12/21/2012 12:00:00 AM

  五、通過TimeZoneInfo實現Utc和Local的轉換

  上面提供的方式雖然簡單,但是功能上確有局限,因為轉換的過程是基于本機當前的時區。這解決不了我在開篇介紹的應用場景:服務端根據訪問者所在的時區(而不是本機的時區)進行時間的轉換。換句話說,我們需要能夠基于任意時區的時間轉換方式,這就可以通過System.TimeZoneInfo

  TimeZoneInfo實際上對原來System.TimeZone類型的一個改進。它是一個可序列化的類型(這一點在分布式場景中進行基于時區的時間處理實現非常重要),表示具體某個時區的信息。它提供了一系列靜態方法供我們對某個DateTime對象進行基于指定TimeZoneInfo的時間轉換,在這我們介紹我們常用的2個:ConvertTimeFromUtc和ConvertTimeToUtc。前者將一個DateTimeKind.Utc或者Unspecified的DateTime時間轉換成基于指定時區的DateTimeKind.Local時間;后者則將一個基于指定時區的DateTimeKind.Local或者DateTimeKind.Unspecified時間象轉化成一DateTimeKind.Utc時間。此外,TimeZoneInfo還提供了兩個靜態屬性Local和Utc表示本地時區和格林威治時區。

   1: [Serializable]
   2: public sealed class TimeZoneInfo : IEquatable<TimeZoneInfo>, ISerializable, IDeserializationCallback
   3: {
   4:     //Others...
   5:     public static DateTime ConvertTimeFromUtc(DateTime dateTime, TimeZoneInfo destinationTimeZone);
   6:     public static DateTime ConvertTimeToUtc(DateTime dateTime, TimeZoneInfo sourceTimeZone);
   7:  
   8:     public static TimeZoneInfo Local { get; }
   9:     public static TimeZoneInfo Utc { get; }
  10: }

  我們照例來做個試驗。還是剛才創建的三個DateTime對象,現在我們分別調用ConvertTimeFromUtc將DateTimeKind.Utc或者DateTimeKind.Unspecified時間轉換成DateTimeKind.Local時間;然后將調用ConvertTimeToUtc將DateTimeKind.Local或者DateTimeKind.Unspecified時間轉換成DateTimeKind.Utc時間。

   1: DateTime endOfTheWorld1 = new DateTime(2012, 12, 21, 0, 0, 0, DateTimeKind.Local);
   2: DateTime endOfTheWorld2 = new DateTime(2012, 12, 21, 0, 0, 0, DateTimeKind.Unspecified);
   3: DateTime endOfTheWorld3 = new DateTime(2012, 12, 21, 0, 0, 0, DateTimeKind.Utc);
   4:  
   5: Console.WriteLine("TimeZoneInfo.ConvertTimeFromUtc(endOfTheWorld2,TimeZoneInfo.Local) = {0}", 
   6:     TimeZoneInfo.ConvertTimeFromUtc(endOfTheWorld2, TimeZoneInfo.Local));
   7: Console.WriteLine("TimeZoneInfo.ConvertTimeFromUtc(endOfTheWorld3,TimeZoneInfo.Local) = {0}\n",
   8:    TimeZoneInfo.ConvertTimeFromUtc(endOfTheWorld3, TimeZoneInfo.Local));
   9:  
  10: Console.WriteLine("TimeZoneInfo.ConvertTimeToUtc(endOfTheWorld1,TimeZoneInfo.Local) = {0}",
  11:     TimeZoneInfo.ConvertTimeToUtc(endOfTheWorld1, TimeZoneInfo.Local));
  12: Console.WriteLine("TimeZoneInfo.ConvertTimeToUtc(endOfTheWorld2,TimeZoneInfo.Local) = {0}",
  13:    TimeZoneInfo.ConvertTimeToUtc(endOfTheWorld2, TimeZoneInfo.Local));

  同上面進行的轉換方式一樣,在向DateTimeKind.Utc時間進行轉換的時候,DateTimeKind.Unspecifed時間被當成DateTimeKind.Local;而在向DateTimeKind.Local時間轉換的時候,DateTimeKind.Unspecifed則被當成DateTimeKind.Utc時間。

   1: TimeZoneInfo.ConvertTimeFromUtc(endOfTheWorld2,TimeZoneInfo.Local) = 12/22/2012 8:00:00 AM
   2: TimeZoneInfo.ConvertTimeFromUtc(endOfTheWorld3,TimeZoneInfo.Local) = 12/22/2012 8:00:00 AM
   3:  
   4: TimeZoneInfo.ConvertTimeToUtc(endOfTheWorld1,TimeZoneInfo.Local) = 12/21/2012 4:00:00 PM
   5: TimeZoneInfo.ConvertTimeToUtc(endOfTheWorld2,TimeZoneInfo.Local) = 12/21/2012 4:00:00 PM

  ConvertTimeFromUtc和ConvertTimeToUtc方法在轉換的時候,如果發現被轉換的時間和需要轉化時間具有相同的DateTimeKind會拋出異常。也就是說,我們不能調用ConvertTimeFromUtc方法并傳入DateTimeKind.Local時間,也不能調用ConvertTimeToUtc方法并傳入DateTimeKind.Urc時間。如右圖所式,我們將一個DateTimeKind.Utc時間(DateTime.UtcNow)傳入ConvertTimeToUtc方法,結果拋出一個ArgumentException異常。錯誤消息為:“The conversion could not be completed because the supplied DateTime did not have the Kind property set correctly.  For example, when the Kind property is DateTimeKind.Local, the source time zone must be TimeZoneInfo.Local. Parameter name: sourceTimeZone”。

image

3
0
 
標簽:ASP.NET
 
 

文章列表

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 大師兄 的頭像
    大師兄

    IT工程師數位筆記本

    大師兄 發表在 痞客邦 留言(0) 人氣()