一.时区的概念
首先来了解时区的概念。为了解决世界不同各地在时间上的差异,人们定义了时区,时区是地球上的区域使用同一个时间定义。人们将时区分为24个,它们是中时区(零时区)、东1-12区,西1-12区。每个时区横跨经度15度,时间正好是1小时。其中中时区就是格林尼治标准时间。以这个时间为基准,东加西减。我们中国虽然跨越了五个时区,但是统一采用北京时间,也就是东八区。这面有几个换算时差的列子。
格林尼治时间 5月31日 0:00:00 则北京时间是 5月31日 8:00:00
北京时间: 5月31日 8:00:00 则格林尼治时间 是 5月31日 0:00:00
北京时间: 5月31日 8:00:00 则东京时间(东九区):5月31日 9:00:00 (东加西减)
北京时间 5月31日 8:00:00 则纽约时间(西五区):先转换成格林尼治时间 5月31日 0:00:00 再在这个时间的基础上减去4个小时得出纽约时间:4月30日 20:00:00
二:.NET的DateTime关于时区的探讨
DateTime有一个Kind属性,该属性的类型为DateTimeKind的枚举。DateTimeKind定义如下,它具有三个枚举值:Unspecified、Utc和Local。后两个分别表示UTC(格林威治时间)和本地时间。Unspecified顾名思义,就是尚未指定具体类型,这是默认值。
2 [ComVisible(true)]
3 public enum DateTimeKind
4 {
5 // 摘要:
6 // The time represented is not specified as either local time or Coordinated
7 // Universal Time (UTC).
8 Unspecified = 0,
9 //
10 // 摘要:
11 // The time represented is UTC.
12 Utc = 1,
13 //
14 // 摘要:
15 // The time represented is local time.
16 Local = 2,
17 }
而且该属性是只读属性,我们能通过构造函数或者DateTime的静态函数SpecifyKind。该方法不会真正去修改一个现有DateTime对象的Kind属性,而是会重新创建一个新的DateTime对象。方法返回的对象具有和指定时间相同的基本属性(年、月、日、时、分、秒和毫秒),该DateTime对象具有你指定的DateTimeKind值。
public static DateTime SpecifyKind(DateTime value, DateTimeKind kind); |
三.几个常用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的对等性问题
接下来,我们来谈谈另外一个比较有意思的问题——两个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对象对象表示相同的时间点。关于时间转换的实现,我们有很多不同的选择。
Console.WriteLine( DateTime.UtcNow.ToLocalTime().Kind); //输出Local |
五.通过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}
", 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}
",
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”。
原文链接:http://www.cnblogs.com/artech/archive/2010/09/04/InsideDateTime_01.html