DateTime.Parse 解析文字時間時區會遺失問題
最近接到一個專案,原本時間欄位大多使用 DateTime,但因為要串另外一套 API,對方是使用 DateTimeOffset。我原本以為只要 JSON 裡面有時間字串,應該就會連時區一起帶著走,結果實際追問題時,還真的踩到時間少了 8 小時的坑。
後來往下追,問題點其實就卡在 DateTime.Parse 這種「把字串重新轉回時間」的流程。尤其當字串裡本來就沒有時區資訊時,這一步很容易把原本上下文中的時區概念弄丟。
心智圖
問題
我這次遇到的程式碼大概像下面這樣:
|
|
表面上看起來沒什麼問題,就是把日期跟時間字串拼起來再比較。但這裡最大的風險是:day + " " + originStopDate.ArrivalTime 這個字串本身沒有任何時區資訊。
也就是說,當你把它重新交給 DateTime.Parse 時,.NET 只知道「這是一個時間」,卻不知道它到底是哪一個時區的時間。
為什麼會發生?
先講結論:這類問題通常不是因為 DateTime.Parse 一定會直接變成 UTC,而是因為它在缺少時區資訊時,常常只會產生 Kind = DateTimeKind.Unspecified 的 DateTime。
|
|
當 Kind 是 Unspecified 時,後續如果你又拿它去做 JSON 序列化、ToUniversalTime()、和 DateTimeOffset 混算,或是交給別的系統處理,就很容易在某個環節被當成本地時間或 UTC,最後時間就偏掉了。
很多人會以為 DateTime.Parse("沒有時區的字串") 會直接變成 UTC。實際上,比較常見的情況是它回傳 Kind = Unspecified。
真正出問題的,是你後面又把這個 Unspecified 拿去做跨系統、跨時區或序列化處理。
微軟文件也有提到,DateTime.Parse 在遇到包含 Z、+08:00、GMT 等時區資訊時,才會依條件解析成 Local 或 Utc;如果沒有,通常就是 Unspecified。
原因整理
以下幾種狀況比較常見:
- 字串本身不含時區資訊,例如
2026-04-28 10:30:00。 - 解析後得到
DateTimeKind.Unspecified。 - 後面又和
DateTimeOffset、UTC 時間或 JSON 序列化混用。 - 系統在某個步驟自動幫你做時區換算,最後差了 8 小時。
參考文章: DateTime.Parse 與時區問題 - 黑暗執行緒
解法
方法 1:如果你真的只能從字串 parse,至少把 offset 補上
原本的寫法:
|
|
這樣 parse 完之後,時間本身通常不帶明確時區資訊。
如果你很確定這筆資料就是台灣時間,至少可以把 offset 明確補上,再改用 DateTimeOffset.Parse:
|
|
如果只是想先做最小修改,這招通常能先把 bug 壓下來。
+08:00。如果系統未來可能支援不同時區,直接把字串硬補 +08:00 就不夠安全。方法 2:直接用 DateTimeOffset,避免丟失 offset
如果資料本身本來就有時區概念,最穩的做法還是直接使用 DateTimeOffset。
|
|
DateTimeOffset 會把時間點和 offset 一起保留下來,對 API 串接、跨時區比較、資料交換都比較安全。
方法 3:不要把日期與時間先拼成字串再 parse
老實說,我自己不太喜歡用 Parse 來做時間運算,尤其是字串還是自己拼出來的時候。因為只要格式、文化設定、時區資訊有一個地方沒對齊,就很容易出 bug。
如果你手上其實有日期和時間的原始欄位,最好直接組成明確的時間物件。
例如你已經知道這是台灣時間:
|
|
這種寫法雖然比較長,但語意很明確,也比較不會因為字串格式問題踩雷。
補充:DateTime 和 DateTimeOffset 差在哪?
這個問題會反覆出現,很多時候不是 parse 本身,而是型別選錯。
DateTime
- 只表示一個日期時間值。
- 可以帶
Kind,但Kind只有Local、Utc、Unspecified這幾種狀態。 - 遇到跨系統交換資料時,很容易因為
Unspecified產生誤解。
DateTimeOffset
- 除了日期時間,還會保留 offset,例如
+08:00。 - 更適合 API、資料交換、跨時區系統。
- 當你需要表達「某個實際時間點」時,比
DateTime更安全。
如果資料會跨 API、跨服務、跨地區流動,優先考慮 DateTimeOffset。
如果只是系統內部單機使用,DateTime 也不是不能用,但至少要統一規則,例如全系統都用 UTC,顯示時再轉成本地時間。
一個簡單示範
下面這段程式碼可以很快看出差異:
|
|
當你把 a 往後再做轉換時,就很容易出現系統各自解讀的問題;但 b 因為把 offset 一起保留下來,可預測性就高很多。
小結
這次踩到的坑,表面上像是 DateTime.Parse 壞掉了,但本質上其實是「沒有時區資訊的字串,被重新 parse 成時間後,又拿去跟有時區語意的資料混用」。
我自己最後的結論很簡單:
- 只要有跨系統、跨 API、跨時區需求,優先使用
DateTimeOffset。 - 如果資料原本沒有時區,就不要假設 parse 完之後系統會幫你猜對。
- 能不用字串拼接做時間運算,就盡量不要用。