先说说几个核心判断:C# 里处理时间戳转 DateTime,最坑的地方不是算法本身,而是毫秒和秒的区分,以及时区上下文。很多开发者在这上面翻过车,归根结底是没搞清楚两个基本问题——时间戳是多少位的?以及你手里那个 DateTime 的 Kind 到底对不对?

时间戳转 DateTime 不能直接除 1000 或硬写 1970-01-01 —— C# 里必须区分毫秒级和秒级,且得用 DateTimeOffset 或校准 DateTimeKind,否则本地时区会悄悄出错。
毫秒级时间戳(常见于 Ja vaScript、C# DateTime.Now.ToUniversalTime().Ticks)
毫秒级时间戳,就是那串 13 位的数字(比如 1717023600000),代表从 Unix 纪元(1970-01-01T00:00:00Z)开始算起的毫秒数。C# 里没有直接“毫秒转 DateTime”的内置函数,得手动构造。但这里有个讲究:
- 最安全的做法是用
DateTimeOffset.FromUnixTimeMilliseconds(),它返回的对象自带 UTC 时区信息,你可以链式调用.UtcDateTime或.LocalDateTime来获取你想要的格式。 - 千万别用
new DateTime(1970,1,1,0,0,0, DateTimeKind.Utc).AddMilliseconds(ts)—— 这个方法虽然也返回一个DateTime,但它的Kind属性是Unspecified,后续只要涉及时区转换,就会出问题。 - 如果非要用
DateTime,记得手动指定DateTimeKind.Utc:DateTime.SpecifyKind(..., DateTimeKind.Utc)。
看个例子:
long ts = 1717023600000; DateTimeOffset dto = DateTimeOffset.FromUnixTimeMilliseconds(ts); DateTime utc = dto.UtcDateTime; // 2024-05-30 07:00:00 DateTime local = dto.LocalDateTime; // 依系统时区,如东八区 → 2024-05-30 15:00:00
秒级时间戳(常见于 Python time.time()、Linux date +%s)
秒级时间戳是 10 位整数(比如 1717023600),C# 直接提供了内置支持:
- 直接用
DateTimeOffset.FromUnixTimeSeconds(),行为和毫秒版一样,但输入是秒。 - 传入负数也是合法的,表示 1970 年之前的时间。
FromUnixTimeMilliseconds同样支持负值。 - 一个常见的坑:不要自己乘 1000 再去调用毫秒方法。虽然结果可能对,但代码可读性差,而且如果没注意类型转换,32 位 int 秒数转 long 毫秒时会截断溢出。
示例:
long secTs = 1717023600;
DateTimeOffset dto = DateTimeOffset.FromUnixTimeSeconds(secTs);
Console.WriteLine(dto.ToString("o")); // 2024-05-30T07:00:00.0000000+00:00
从 DateTime 反推时间戳时,务必确认时区意图
很多人卡在“为什么我转回去的数字和原来不一样”,根源就是没搞清楚原始时间戳代表的是 UTC 还是本地时间:
DateTimeOffset.ToUnixTimeMilliseconds()总是基于 UTC 时间计算,所以安全。dateTime.ToUniversalTime().Subtract(...).TotalMilliseconds这个路子也行,但前提是dateTime.Kind != DateTimeKind.Unspecified。否则ToUniversalTime()会按本地时区解释,偏移就错了。- 如果原始时间戳本身就是按本地时间定义的(这种情况极少见),那只能手动用
TimeZoneInfo.Local.GetUtcOffset()补偿,但说实话,这违背了 Unix 时间戳的基本语义,不推荐这么干。
注意 DateTimeOffset 和 DateTime 的序列化差异
Web API 或 JSON 序列化时,DateTimeOffset 默认输出带偏移(比如 "2024-05-30T07:00:00+00:00"),而 DateTime 的 Kind 信息不会进 JSON,反序列化后很容易丢失时区上下文:
- 前后端约定用时间戳通信时,建议全程用
DateTimeOffset,避免歧义。 - Entity Framework Core 6+ 支持
DateTimeOffset映射到 SQL Server 的datetimeoffset类型;如果用的是旧版本或 SQLite,需要自己处理偏移存储。 - 日志中打印时间,优先用
dto.ToString("o"),比dt.ToString("o")更明确,不会引发时区猜测。
最常被忽略的一点:时间戳本身没有时区概念,但 C# 的 DateTime 有 Kind 属性。一旦构造方式用错了,后面所有比较、格式化、序列化都会连锁出错 —— 不是数字不对,而是语义错了。