國內大多數開發者使用的電腦,都是使用的北京時間,日常開發的過程中其實並沒有什麼不便;不過,等遇到了阿里雲等雲伺服器,系統預設使用的時間大多為UTC時間,這個時候,時區和時間的問題,就是不容忽視的大問題。
概念
首先明確一點,對於一個時刻,不管你用UTC時間還是UTC+8的時間來表示,本質上是一個時刻,就是一樣的。我們處理日期和時間的目標,也是為了保證這個時刻不會因為時區的不同出現對不上的情況。
DateTime與DateTimeOffset
.NET中表示時刻的資料型別有這兩個(新出的Date和Time不作討論),關於這兩個資料型別,已經有同學寫的很清楚了,阿里雲很多伺服器使用的時間為UTC時間,這個時候,如果使用DateTime,是很難說清楚時區(Kind只有UTC、Local還有未指定,不支援特定的某個時區),因此我們應當優先使用DateTimeOffset。
TimeZoneInfo
用於跨時區的情況下,時區的資訊是很重要的,.NET中使用TimeZoneInfo這個類表示時區的資訊。該類提供了一些靜態方法,可以用於查詢時區和建立時區等等。最早我是傾向於使用這些方法找到東八區的資訊的,但是我發現諸如ConvertTimeBySystemTimeZoneId
和FindSystemTimeZoneById
的方法,都依賴於系統中的定義,不同的系統可能還不一樣,自己定義是比較保險的,於是,我使用了CreateCustomTimeZone
來新建一個時區。
Unix時間戳是比較於1970年的UTC標準時間,因此在處理的過程中,DateTime的時間表示應當將它轉換為UTC時間,以下的程式碼,是使用TimeZoneInfo實現時間轉換的,使用的是DateTime資料型別。如果改用DateTimeOffset,這個型別對轉換為Unix時間戳更加友好。
internal static class DateTimeExtension
{
private static readonly TimeZoneInfo gmt8 = TimeZoneInfo.CreateCustomTimeZone("GMT+8", TimeSpan.FromHours(8), "China Standard Time", "(UTC+8)China Standard Time");
public static long ToUnixTime(this DateTime datetime)
{
DateTime dateTimeUtc = datetime;
if (datetime.Kind != DateTimeKind.Utc)
{
dateTimeUtc = datetime.ToUniversalTime();
}
if (dateTimeUtc.ToUniversalTime() <= DateTime.UnixEpoch)
{
return 0;
}
return (long)(dateTimeUtc - DateTime.UnixEpoch).TotalMilliseconds;
}
public static DateTime ToDateTime(this long unixTimestamp)
{
DateTime time = DateTime.UnixEpoch.AddMilliseconds(unixTimestamp);
return TimeZoneInfo.ConvertTimeFromUtc(time, gmt8);
}
public static DateTime ToDateTime(this long unixTimestamp, int timezone)
{
DateTime time = DateTime.UnixEpoch.AddMilliseconds(unixTimestamp);
return time.AddHours(timezone);
}
}
其實,只要時區是正確的,那麼可以也可以使用網友提供的方法進行轉換。
// Code from https://stackoverflow.com/questions/5615538/parse-a-date-string-into-a-certain-timezone-supporting-daylight-saving-time
public DateTimeOffset ParseDateExactForTimeZone(string dateTime, TimeZoneInfo timezone)
{
var parsedDateLocal = DateTimeOffset.ParseExact(dateTime, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
var tzOffset = timezone.GetUtcOffset(parsedDateLocal.DateTime);
var parsedDateTimeZone = new DateTimeOffset(parsedDateLocal.DateTime, tzOffset);
return parsedDateTimeZone;
}
實踐指南
處理日期與時間的過程中,如果加入了TimeZoneInfo的情況下會使得程式變得非常麻煩,特別是各種TimeZone的Id和名稱,不同系統也不統一的情況下,容易出現各種各樣的問題。我想的就是避免用它,說說我的處理原則吧。
- 日期時間不使用DateTime類,全部使用DateTimeOffset型別
- 系統的內部處理,全部使用UTC標準時間進行資料表示
- 對於字串的轉換為DataTimeOffset的情況,顯式指定時區的小時偏移量
- 直接使用時間的加減,避免使用時區的資訊轉換導致的程式碼複雜度增加
- 【可選】如果不用考慮2038年的情況下,可以考慮Unix時間戳簡化時間表示
直接貼上我現在使用的程式碼段,思路就是在強制給字串表示的時間,加上UTC標準時區資訊,然後再修正時差。
public static class DateTimeExtension
{
public static long? ParseUnixTimeMillisecondsWithTimeZone(string datetimeString, string format = "yyyyMMddHHmmss", int timezoneOffset = 8)
{
//注意這裡非常關鍵的引數DateTimeStyles.AssumeUniversal,就是設定資料都是UTC的,不管是不是,都強行指定為UTC,然後再按照時區的資訊調整為正確的時間。
//給定的資料是東八區時間,但是加上這個引數,實際上的時間就會提前了8個小時,因此需要在後面的資料中直接減去8個小時,如果是其他地區的時間,那麼也是一樣操作。
if (!DateTimeOffset.TryParseExact(datetimeString, format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTimeOffset time)) return null;
DateTimeOffset dateTimeUtcOffset = time.AddHours(-timezoneOffset);
return dateTimeUtcOffset.ToUnixTimeMilliseconds();
}
public static DateTimeOffset ToDateTime(this long unixTimestamp) => DateTimeOffset.FromUnixTimeMilliseconds(unixTimestamp);
}
對於ASP.NET CORE,JSON.NET會自動處理符合ISO8601規範的日期格式,只要指定資料型別為DateTimeOffset,就能夠準確轉換了。
參考
- https://stackoverflow.com/questions/5615538/parse-a-date-string-into-a-certain-timezone-supporting-daylight-saving-time
- https://stackoverflow.com/questions/62433342/c-sharp-datetime-converting-a-datetimeoffset-to-another-timezone
- https://stackoverflow.com/questions/63153809/parse-string-into-datetimeoffset-while-assuming-timezone