DateTime.MinValue や new DateTime() は危険

昨日、コロナ接触確認アプリ COCOA が、バージョンアップで起動不能になる と言う現象が確認されたらしく、この問題の検証が GitHub 上で行われていました(COCOA v1.4.0 が起動しない - Issue #517 - cocoa-mhlw/cocoa)。この Issue によると、原因は .NET の DateTime 構造体の扱いにあったようです。

私自身も過去に似たような経験をした事がありますが、DateTime.MinValue や new DateTime()(引数無しで初期化)を使用するのは危険です。これら自体に直接的な問題があると言う訳ではないのですが、タイムゾーン間での時刻変換を実施すると例外が発生する時があります。

この問題はシリアライズの際に顕在化する事が多いようです。これは、シリアライズの際に DateTime オブジェクトの内容を UTC 時刻に変換する処理が含まれがちなせいで、例えば DateTime.MinValue の含まれるオブジェクトを .NET 標準で提供されている DataContractJsonSerializer クラスでシリアライズすると、UTC 時刻より早いタイムゾーンに属する国・地域では、例外が発生します(参考: Why can DateTime.MinValue not be serialized in timezones ahead of UTC?)。

私自身に関しては、対策として最近は ソフトウェアの初期リリース以前の時刻等、適当な時刻をそのプロジェクトでの最小値・初期値として決めてしまう と言う方針を取るようになりました。例えば、プライベートリポジトリで開発しているプロジェクト中の設定内容を保持するクラスで、以下のような記述が見つかりました。

[DataMember]
public DateTime LastChecked
 {
    get => Get(() => new DateTime(2010, 7, 7, 9, 0, 0));
    set => Set(value);
}

このプロパティは、既に値が設定されていればその値を、そうでなければ new DateTime(2010, 7, 7, 9, 0, 0) と言うオブジェクトを生成して返すと言う挙動を示します(Get/Set の実装詳細は SerializableBase を参照)。プロジェクトの規模や時刻の性質等もあるので必ずしも適用できると言う訳ではないでしょうが、前述したような不都合を発生させにくい方法として気に入っています。ちなみに、2010 年 7 月 7 日と言うのは、弊社の最初のソフトウェアである CubePDF をリリースした日付です。

王道的な対応で言うと、タイム ゾーン間での時刻変換の内、ローカル時刻と UTC 時刻の変換に関しては DateTime 構造体が提供する ToUniversalTime および ToLocalTime メソッドを使用する方法が挙げられます。例えば、DateTime.MinValue.ToUniversalTime() は、予想に反して、例外を発生させずに正常終了します(日時の値は変更されずにタイムゾーンの値のみ UTC になる)。ただ、DataContractJsonSerializer の事例のようにライブラリ側で変換処理が実行されて例外が発生する事も多いため、万全を期すのであればライブラリに指定する全ての DateTime オブジェクトに対して ToUniversalTime 等を実施しなければならず、結構な負担となります。

それ以外では、DateTime ではなく DateTime? を使用して、未設定時は null にしておくと言う方針が取られる事も多いかと思います。