還在用 DateTime 嗎?試試 DateTimeOffset 吧
- 2021-04-25
- 22479
- 0
廣大的 .NET 開發者一定都用過 DateTime ,取得現在的時間就很自然的使用 DateTime.Now
,看似美好的日子竟然會因為雲端的普及而開始受到迫害,雲端平台的服務因為是全球性質因此時區通常都定在國際標準時間 UTC +0(以下稱為 Universal Time) ,所以為了時區的正確性,我們開始改變了時間的寫法,由 DateTime.Now
換成了 DateTime.UtcNow
,並且保持一個開發原則,進資料庫儲存的都是 Universal Time 顯示時再調整為適合的本地時區(以下稱為 Local Time 並且使用台灣時區 UTC+8)顯示,在這個原則限制之下世界終於恢復了平靜,但真的是這樣嗎?…
情境說明
網頁上有一個日期欄位與時間欄位讓使用者自行選擇,選擇完畢的時間與日期要存進資料庫,使用者在網頁上選擇了 2021-04-25 21:00:00
,我們預期存入資料庫的時間是 2021-04-25 13:00:00
,顯示的時候再將時間轉換為 Local Time 即可,於是我們的程式就大概是這樣。
var dt=new DateTime(2021,4,20);
var ti=new TimeSpan(21,0,0);
//使用者選的預期台灣時間
var nd=(dt+ti);
//存入資料庫的 UTC 時間
nd.ToUniversalTime();
然後我們開開心心的把程式上傳到雲端上,時間卻錯亂了😱
上雲以後使用者選擇的本地時間看起來是正常,但是 ToUniverslTime()
取得的時間竟然沒有-8
導致存入資料庫的時間與預期的本地時間整整多了八小時,這還得了…到底發生了什麼事!
DateTime 的原罪
DateTime 有一個 Kind 屬性可以記錄這個時間是 UTC , Local, Unspecified 看似可以解決我們的問題,但其實有些問題,做一個簡單的示範
//取得本地時間(台灣)
var saveNow = DateTime.Now;
//output: 2021-04-25 17:26:56, Kind = Local
//取得國際標準時間 Kind= UTC
var saveUtcNow = DateTime.UtcNow;
//output: 2021-04-25 09:26:56, Kind = Utc
DateTime myDt;
//把 本地時間的 Kind 從 Local 轉為 UTC"
myDt = DateTime.SpecifyKind(saveNow, DateTimeKind.Utc);
myDt.ToLocalTime();
//output: 2021-04-26 01:26:56, Kind = Local
myDt.ToUniversalTime();
//output: 2021-04-25 17:26:56, Kind = Utc
上面的程式碼 UTC 時間是拿來比對用的,所以我們從 Local Time 來講,系統抓出的本地時間是 2021-04-2517:26:56 將 Kind 自行調整為 Local 後再次使用 ToLocalTime()
取得的時間卻變成 2021-04-26 01:26:56 ,也就是和真的本地時間多了八小時,使用 ToUniversalTime()
時間卻是實際上的本地時間,這是為什麼呢?🤔
我們以為的 Local Time,在手動改變 Kind 值後就搖身一變的變成了 Universal Time 所以當你使用 DateTime
內建方法要轉回 Local Time ,系統知道你目前 OS 的時區是 UTC+8 就很貼心的再幫你加上 8小時,那你想 -8 使用的 ToUniversalTime()
就完全沒變,因為你告訴程式你這時間本來就是 UTC 的,最可怕的是 Local Time 是執行程式定義的!本案例就是因為上傳到雲端的 Universal Time 環境後,在程式執行時 Local Time 同等於 Universal Time 導致預期的 -8 沒有作用,更不用說很多時候的 DateTime Kind 是 Unspecified。
官方範例完整程式碼可參考此連結: 點擊觀看
DateTimeOffset 的救贖
在問題發生的時候我就覺得 .NET 一定有內建的方法可以解,.NET 針對時間和曆法支援程度那麼高沒道理沒有官方解決方案,所以沒多久就搜尋到了 DateTimeOffset
這個結構,它可以儲存 Local Time 和 Universal Time 的時間差異,所以對於時區轉換的議題可以更精準,且可以轉回 DateTime 所以你不需要改太多的程式,就可以完美修正了。
DateTimeOffset 早在 .NET Framework 2.0 就出現,它的出現就是要解決 DateTime 在時區轉換與比對的時候的問題,如果程式需要考慮到時區轉換時強烈建議使用,官方還有寫文章來詳細的描述你該怎麼選「 Choose between DateTime, DateTimeOffset, TimeSpan, and TimeZoneInfo」現在讓我們直接寫程式看一下 DateTimeOffset
的差異。
var dt=new DateTime(2021,4,20);
var ti=new TimeSpan(21,0,0);
//使用者選的預期台灣時間
var nd=(dt+ti);
//使用 DateTimeOffset 並且明確表明我們時區是 +8
var offset=new DateTimeOffset(nd,new TimeSpan(8,0,0));
//存入資料庫的 UTC 時間
offset.ToUniversalTime();
本地開發環境沒問題,那在 Universal Time 時區的環境呢?
看執行結果就可以明確瞭解,使用 DateTimeOffset 後時間的資訊就包含了與 UTC 的差異,所以你會看到時間後面帶了+08:00
既然有了這資訊,要回到 UTC+0 就有依據了,所以轉來轉去都不會出事,其實 DateTimeOffset 使用上是很簡單的,只是底層的觀念很多,時區本來就是麻煩的事情,好在 .NET 早就幫我們處理好了。
同場加映
更聰明的時區差異取得方式
前面的程式碼範例使用了 new TimeSpan(8,0,0)
的方式加上了時區,但如果網站確實是多國語系的,哪可能這樣寫死,所以你可以利用這樣的方式來處理
var offnd = new DateTimeOffset(nd, TimeZoneInfo.FindSystemTimeZoneById("Taipei Standard Time").BaseUtcOffset);
其中的 Taipei Standard Time
只要帶入使用者的時區就可以完美的解決了,建議將其寫成擴充方法方便以後的使用(DateTimeOffset 有提供 DateTime 屬性,可以讓你取回 DateTime),在 MVC 或 Razor Pages 的應用上還可以使用 Display Template 讓網站可以輕鬆自動轉換。
到底該 Now 還是 UtcNow ?
時區轉換不是問題了,那額外來各新議題,我們到底應該用 Now
還是 UtcNow
呢? 直接反組譯看看👀
public static DateTime Now
{
get
{
DateTime utc = UtcNow;
bool isAmbiguousLocalDst = false;
long offset = TimeZoneInfo.GetDateTimeNowUtcOffsetFromUtc(utc, out isAmbiguousLocalDst).Ticks;
long tick = utc.Ticks + offset;
if (tick > DateTime.MaxTicks)
{
return new DateTime(DateTime.MaxTicks, DateTimeKind.Local);
}
if (tick < DateTime.MinTicks)
{
return new DateTime(DateTime.MinTicks, DateTimeKind.Local);
}
return new DateTime(tick, DateTimeKind.Local, isAmbiguousLocalDst);
}
}
Now
其實就是先取得 UtcNow
再轉成 Local Time 所以你覺得我們該用什麼呢?
回應討論