System.DateTime详解

  最近一直在负责公司内部框架的升级工作,今天对一个小问题进行了重新思考——时间的处理。具体来说,是如何有效地进行时间的处理以提供对跨时区的支持。对于一个分布式的应用来说,倘若客户端和服务端部署与不同的地区,在对时间进行处理的时候,就需要考虑时区的问题。以我们现在的一个项目为例,这是一个为澳大利亚某机构开发的一个基于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时间。

时间: 2024-10-24 07:01:18

System.DateTime详解的相关文章

System.DateTime 详解(续)

在<System.DateTime 详解>一文中,我们从跨时区的角度剖析了我们熟悉的System.DateTime类型.如果你还是采用传统的ADO.NET编程方式,并使用DataSet作为数据实体,可能你会熟悉System.Data.DataSetDateTime这么一个类型.这个类型也是为实现跨时区场景下对时间处理而设计的,为了对前文的补充,这篇文章就来谈谈基于DataSet的时间处理问题. 一.你是否关注过DataColumn的DateTimeMode属性 在ADO.NET编程模型中,Da

一起谈.NET技术,System.DateTime 详解(续)

在<System.DateTime 详解>一文中,我们从跨时区的角度剖析了我们熟悉的System.DateTime类型.如果你还是采用传统的ADO.NET编程方式,并使用DataSet作为数据实体,可能你会熟悉System.Data.DataSetDateTime这么一个类型.这个类型也是为实现跨时区场景下对时间处理而设计的,为了对前文的补充,这篇文章就来谈谈基于DataSet的时间处理问题. 一.你是否关注过DataColumn的DateTimeMode属性 在ADO.NET编程模型中,Da

一起谈.NET技术,System.DateTime详解

最近一直在负责公司内部框架的升级工作,今天对一个小问题进行了重新思考--时间的处理.具体来说,是如何有效地进行时间的处理以提供对跨时区的支持.对于一个分布式的应用来说,倘若客户端和服务端部署与不同的地区,在对时间进行处理的时候,就需要考虑时区的问题.以我们现在的一个项目为例,这是一个为澳大利亚某机构开发的一个基于Smart Client应用(Windows Form客户端),服务器部署于墨尔本,应用的最终用户可能需要跨越不同的州.澳洲地广人稀,不同的州也有可能会跨越不同的时区.假设数据库并不支持

谁用光了磁盘?Docker System命令详解

本文讲的是谁用光了磁盘?Docker System命令详解, 为了保证可读性,本文采用意译而非直译. 用了一段时间Docker后,会发现它占用了不少硬盘空间.还好Docker 1.13引入了解决方法,它提供了简单的命令来查看/清理Docker使用的磁盘空间. 本文通过一个简单的示例,可以证明Docker能够很快地将磁盘占满.该示例通过play-with-docker.com运行.点击Add new instance即可创建新的实例,该实例安装了最新版的Docker 17.03.这篇博客主要讨论磁

PHP启动windows应用程序、执行bat批处理、执行cmd命令的方法(exec、system函数详解)_php实例

exec 或者 system 都可以调用cmd 的命令 直接上代码: 复制代码 代码如下: <?php /** 打开windows的计算器 */ exec('start C:WindowsSystem32calc.exe'); /** php生成windows的批处理文件后,再执行这个批处理文件*/ $filename = 't.bat'; $somecontent = 'C: '; $somecontent .= 'cd "C:/Program Files/MySQL-Front&quo

System.Data.DataTable计算功能详解

using System; using System.ComponentModel; using System.Data; using System.Windows.Forms; namespace WindowsApplication1 ...{ public partial class Form1 : Form ...{ public Form1() ...{ InitializeComponent(); } private void button1_Click(object sender,

详解在C++中显式默认设置的函数和已删除的函数的方法_C 语言

在 C++11 中,默认函数和已删除函数使你可以显式控制是否自动生成特殊成员函数.已删除的函数还可为您提供简单语言,以防止所有类型的函数(特殊成员函数和普通成员函数以及非成员函数)的参数中出现有问题的类型提升,这会导致意外的函数调用. 显式默认设置的函数和已删除函数的好处 在 C++ 中,如果某个类型未声明它本身,则编译器将自动为该类型生成默认构造函数.复制构造函数.复制赋值运算符和析构函数.这些函数称为特殊成员函数,它们使 C++ 中的简单用户定义类型的行为如同 C 中的结构.也就是说,可以创

C++的get()函数与getline()函数使用详解_C 语言

C++ get()函数读入一个字符 get()函数是cin输入流对象的成员函数,它有3种形式:无参数的,有一个参数的,有3个参数的. 1) 不带参数的get函数 其调用形式为 cin.get() 用来从指定的输入流中提取一个字符(包括空白字符),函数的返回值就是读入的字符. 若遇到输入流中的文件结束符,则函数值返回文件结束标志EOF(End Of File),一般以-1代表EOF,用-1而不用0或正值,是考虑到不与字符的ASCII代码混淆,但不同的C ++系统所用的EOF值有可能不同. [例]

C语言中的sscanf()函数使用详解_C 语言

sscanf() - 从一个字符串中读进与指定格式相符的数据. 函数原型: Int sscanf( string str, string fmt, mixed var1, mixed var2 ... ); int scanf( const char *format [,argument]... ); 说明: sscanf与scanf类似,都是用于输入的,只是后者以屏幕(stdin)为输入源,前者以固定字符串为输入源. 其中的format可以是一个或多个 {%[*] [width] [{h |