System.DateTime 详解(续)
在《System.DateTime 详解》一文中,我们从跨时区的角度剖析了我们熟悉的System.DateTime类型。如果你还是采用传统的ADO.NET编程方式,并使用DataSet作为数据实体,可能你会熟悉System.Data.DataSetDateTime这么一个类型。这个类型也是为实现跨时区场景下对时间处理而设计的,为了对前文的补充,这篇文章就来谈谈基于DataSet的时间处理问题。
一、你是否关注过DataColumn的DateTimeMode属性
在ADO.NET编程模型中,DataColumn代表DataTable的一个数据列,大家在熟悉不过了。不过,是否有人关注过一个名称为DateTimeMode属性,该属性在DataColumn中的定义如下:
1: public class DataColumn : MarshalByValueComponent
2: {
3: //Others...
4: public DataSetDateTime DateTimeMode { get; set; }
5: }
从上面的代码我们可以看出,DateTimeMode属性的类型为DataSetDateTime,这实际上是一个枚举类型。下面给出了DataSetDateTime的定义,该枚举一共包含4个枚举值:Local、Utc、Unspecified和UnspecifiedLocal。
1: public enum DataSetDateTime
2: {
3: Local = 1,
4: Unspecified = 2,
5: UnspecifiedLocal = 3,
6: Utc = 4
7: }
如果你读过前文,了解了DateTimeKind,技术你之前不曾关注过这个类型,也会猜出DataColumn用类型为DataSetDateTime的DateTimeMode属性指定时间的Kind。那么对于不同的DateTimeMode设置之间有何差异?为什么DataSetDateTime提供了另一个额外的成员UnspecifiedLocal呢?
二、不同的DateTimeMode设置对DateTimeKind的影响
这个问题很简单,对于数据类型为DateTime的DataColumn,如果你对DateTimeMode属性设置了不同的DataSetDateTime枚举值,当你对其赋值的时候,系统会自动将设置的时间转换成相应DateTimeKind的时间。下面的列表提供了基于不同DataSetDateTime枚举值采用的的转换规则:
- DataSetDateTime.Local: 对于DateTimeKind.Local时间,不做任何转换;对于DateTimeKind.Utc时间,基于时区偏移量进行转换,并将Kind属性转换成DateTimeKind.Local;对于DateTimeKind.Unspecified,直接将Kind属性转换成DateTimeKind.Local,时间值(年、月、日、时、分、秒、毫秒等)保持不变;
- DataSetDateTime.Utc: 对于DateTimeKind.Utc时间,不做任何转换;对于DateTimeKind.Local时间,基于时区偏移量进行转换,并将Kind属性转换成DateTimeKind.Utc;对于DateTimeKind.Unspecified,直接将Kind属性转换成DateTimeKind.Utc,时间值(年、月、日、时、分、秒、毫秒等)保持不变;
- DataSetDateTime.Unspecified|UnspecifiedLocal:对于任何DateTimeKind类型的时间,直接将Kind属性转换成DateTimeKind.Unspecified,时间值(年、月、日、时、分、秒、毫秒等)保持不变。
在这里我需要强调一下,在前文中我们提到:不同是调用DateTime的ToLocalTime/ToUtcTime方法,还是调用TimeZoneInfo的ConvertToUtcTime/ConvertFromUtcTime,如果将DateTimeKind.Unspecified转换成DateTimeKind.Local时间,实际上是将其当成DateTimeKind.Utc时间;反之,如果转换成DateTimeKind.Utc时间,则当成是DateTimeKind.Local时间。但是,在这里DateTimeKind.Unspecified的时间值会保留,改变的仅仅是Kind属性。
三、一个简单的例子
为了加深对上述转换规则的理解,我写了一个简单的例子。首先我创建了一个ContractDataSet的强类型的DataSet,里面具有一个Contact数据表表示一个联系人。Contact数据的结构如右图所示:处理表示Id和名称的两个字段之外,我添加了四个DateTime类型的字段表示生日。它们分别是:LocalBirthday、UtcBirthday、UnspecifiedBirthday和UnspecifiedLocalBirthday,前缀表示该数据列采用的DateTimeMode。
然后,我写了下面三个辅助的方法:CreateContact通过传入的表示生日的DateTime创建一个ContractDataSet,DisplayBirthday分别将上诉四个字段的时间和Kind打印出来。
1: static ContactDataSet CreateContact(DateTime birthday)
2: {
3: var ds = new ContactDataSet();
4: var row = ds.Contact.NewContactRow();
5: row.Id = Guid.NewGuid().ToString();
6: row.Name = "Foo";
7: SetBirthDay(row,birthday);
8: ds.Contact.AddContactRow(row);
9: return ds;
10: }
11: static void SetBirthDay(ContactDataSet.ContactRow row,DateTime birthDay)
12: {
13: row.LocalBirthDay = birthDay;
14: row.UtcBirthDay = birthDay;
15: row.UnspecifiedBirthDay = birthDay;
16: row.UnspecifiedLocalBirthDay = birthDay;
17: }
18: static void DispalyBirthday(ContactDataSet ds)
19: {
20: var row = ds.Contact[0];
21: Console.WriteLine("\tLocal: \t\t\t{0}", row.LocalBirthDay);
22: Console.WriteLine("\tUtc: \t\t\t{0}", row.UtcBirthDay);
23: Console.WriteLine("\tUnspecified: \t\t{0}", row.UnspecifiedBirthDay);
24: Console.WriteLine("\tUnspecifiedLocal: \t{0}\n", row.UnspecifiedLocalBirthDay);
25:
26: Console.WriteLine("\tLocal: \t\t\t{0}", row.LocalBirthDay.Kind);
27: Console.WriteLine("\tUtc: \t\t\t{0}", row.UtcBirthDay.Kind);
28: Console.WriteLine("\tUnspecified: \t\t{0}", row.UnspecifiedBirthDay.Kind);
29: Console.WriteLine("\tUnspecifiedLocal: \t{0}\n", row.UnspecifiedLocalBirthDay.Kind);
30: }
我们的实例程序是这样的:分别创建基于三种不同的DateTimeKind的DateTime对象,并据此创建三个ContractDataSet对象。最后调用DisplayBirthday方法将4个基于不同DateTimeMode的字段的时间和DateTimeKind打印出来。
1: var ds1 = CreateContact(new DateTime(1981, 8, 24, 0, 0, 0, DateTimeKind.Local));
2: var ds2 = CreateContact(new DateTime(1981, 8, 24, 0, 0, 0, DateTimeKind.Unspecified));
3: var ds3 = CreateContact(new DateTime(1981, 8, 24, 0, 0, 0, DateTimeKind.Utc));
4:
5: Console.WriteLine("DateTimeKind.Local");
6: DispalyBirthday(ds1);
7: Console.WriteLine("DateTimeKind.Unspecified");
8: DispalyBirthday(ds2);
9: Console.WriteLine("DateTimeKind.Utc");
10: DispalyBirthday(ds3);
最终的输出结果证实了我们上述的关于时间转换规则的结论:
1: DateTimeKind.Local
2: Local: 8/24/1981 12:00:00 AM
3: Utc: 8/23/1981 4:00:00 PM
4: Unspecified: 8/24/1981 12:00:00 AM
5: UnspecifiedLocal: 8/24/1981 12:00:00 AM
6:
7: Local: Local
8: Utc: Utc
9: Unspecified: Unspecified
10: UnspecifiedLocal: Unspecified
11:
12: DateTimeKind.Unspecified
13: Local: 8/24/1981 12:00:00 AM
14: Utc: 8/24/1981 12:00:00 AM
15: Unspecified: 8/24/1981 12:00:00 AM
16: UnspecifiedLocal: 8/24/1981 12:00:00 AM
17:
18: Local: Local
19: Utc: Utc
20: Unspecified: Unspecified
21: UnspecifiedLocal: Unspecified
22:
23: DateTimeKind.Utc
24: Local: 8/24/1981 8:00:00 AM
25: Utc: 8/24/1981 12:00:00 AM
26: Unspecified: 8/24/1981 12:00:00 AM
27: UnspecifiedLocal: 8/24/1981 12:00:00 AM
28:
29: Local: Local
30: Utc: Utc
31: Unspecified: Unspecified
32: UnspecifiedLocal: Unspecified
四、DataSetDateTime.Unspecified V.S. DataSetDateTime.UnspecifiedLocal
到不前为止,我们貌似还看不到DataSetDateTime.Unspecified和DataSetDateTime.UnspecifiedLoca的差别。实际上,它们的差别体现在序列化上面:DataSetDateTime.UnspecifiedLoca在序列化的时候会保留基于当前时区的偏移量,而DataSetDateTime.Unspecified则不会。这个结论我也可以实例来证实,为此我写了如下一段代码对ContactDataSet进行序列化,并将序列化后的XML打印出来。
1: var ds = CreateContact(new DateTime(1981, 8, 24, 0, 0, 0));
2: using (MemoryStream stream = new MemoryStream(1000))
3: {
4: ds.WriteXml(stream);
5: Console.WriteLine(ASCIIEncoding.ASCII.GetString(stream.GetBuffer()).Trim());
6: }
从输出的结果我们可以看出UnspecifiedBirthday和UnspecifiedLocalBirday之间的差别,后者有+8的偏移量,前者没有。
1: <ContactDataSet xmlns="http://tempuri.org/ContactDataSet.xsd">
2: <Contact>
3: <Id>a1548a6b-9b8b-4799-bc10-31337e70c831</Id>
4: <Name>Foo</Name>
5: <UnspecifiedLocalBirthDay>1981-08-24T00:00:00+08:00</UnspecifiedLocalBirthDay>
6: <UnspecifiedBirthDay>1981-08-24T00:00:00</UnspecifiedBirthDay>
7: <UtcBirthDay>1981-08-24T00:00:00Z</UtcBirthDay>
8: <LocalBirthDay>1981-08-24T00:00:00+08:00</LocalBirthDay>
9: </Contact>
10: </ContactDataSet>