Cruel DateTime vs serialization
Недавно столкнулся с проблемой сериализации DateTime
. protobuf-net, как и BinaryFormatter
не сохраняют тип даты, перечисление DateTimeKind
. В результате после чтения из архива тип даты становится Unspecified
. Вот выдержка из исходного кода структуры DateTime
:
// This value type represents a date and time. Every DateTime // object has a private field (Ticks) of type Int64 that stores the // date and time as the number of 100 nanosecond intervals since // 12:00 AM January 1, year 1 A.D. in the proleptic Gregorian Calendar. // // Starting from V2.0, DateTime also stored some context about its time // zone in the form of a 3-state value representing Unspecified, Utc or // Local. This is stored in the two top bits of the 64-bit numeric value // with the remainder of the bits storing the tick count. This information // is only used during time zone conversions and is not part of the // identity of the DateTime. Thus, operations like Compare and Equals // ignore this state. This is to stay compatible with earlier behavior // and performance characteristics and to avoid forcing people into dealing // with the effects of daylight savings. Note, that this has little effect // on how the DateTime works except in a context where its specific time // zone is needed, such as during conversions and some parsing and formatting // cases. // // There is also 4th state stored that is a special type of Local value that // is used to avoid data loss when round-tripping between local and UTC time. // See below for more information on this 4th state, although it is // effectively hidden from most users, who just see the 3-state DateTimeKind // enumeration. // // For compatability, DateTime does not serialize the Kind data when used in // binary serialization. // // For a description of various calendar issues, look at // // Calendar Studies web site, at // http://serendipity.nofadz.com/hermetic/cal_stud.htm. // // [StructLayout(LayoutKind.Auto)] [Serializable] public struct DateTime : IComparable, IFormattable, IConvertible, ISerializable, IComparable,IEquatable {
Как видно из описания, дата представляет собой число типа Int64
в котором хранится количество 100нс интервалов от начала времен - 12:00 AM January 1, year 1 A.D. in the proleptic Gregorian Calendar
. А вот до какой точки - здесь уже интересней. В случае DateTimeKind.Utc
- до Гринвича, DateTimeKind.Local
- до времени в локальной для операционной системы\программы\потока зоне. И последнее значение - DateTimeKind.Unspecified
, до куда - неизвестно.
На что влияет тип даты? В первую очередь на методы ToLocalTime
и ToUniversalTime
, потом уже и на форматирующие методы. Самое неприятное происходит при вызове этих двух методов для дат с типом DateTimeKind.Unspecified
- ToLocalTime
считает, что дата имеет тип DateTimeKind.Utc
, а ToUniversalTime
- что тип DateTimeKind.Local
. Логично, правда? В результате если сериализировать DateTime.UtcNow
, вычитать его обратно и преобразовать в DateTimeKind.Utc
методом ToUniversalTime
- получаем сдвиг на временную зону. При этом ToLocalTime
вернет правильный результат.
Обойти это недоразумение можно с помощью статического метода DateTime.SpecifyKind
.
using System; namespace CSharpLanguageInv { internal class Program { private static void Main(string[] args) { var now = DateTime.UtcNow; var unspecified = DateTime.SpecifyKind(now, DateTimeKind.Unspecified); var localTime = unspecified.ToLocalTime(); var universalTime = unspecified.ToUniversalTime(); Console.WriteLine("Now = " + now/*.Ticks*/); Console.WriteLine("unspecified = " + unspecified/*.Ticks*/); Console.WriteLine("localTime = " + localTime/*.Ticks*/); Console.WriteLine("universalTime = " + universalTime/*.Ticks*/); Console.WriteLine("Now - unspecified = " + (now/*.Ticks*/ - unspecified/*.Ticks*/)); Console.ReadLine(); } } }