结论:用 TimeZoneInfo 转换时区必须显式指定源时区,不能依赖本地系统时区或字符串解析;UTC 时间必须用 DateTimeKind.Utc 标记,否则 ConvertTimeFromUtc 等方法会静默出错。

直接说结论:用 TimeZoneInfo 转换时区必须显式指定源时区,不能依赖本地系统时区或字符串解析;UTC 时间必须用 DateTimeKind.Utc 标记,否则 ConvertTimeFromUtc 等方法会静默出错。
为什么 DateTime 本身不带时区信息?
DateTime 是个“裸时间值”,只有 Kind 属性(Unspecified / Local / Utc)标记语义,不包含偏移量或时区规则。这意味着:
-
new DateTime(2024, 6, 1, 12, 0, 0)默认是Unspecified,传给ConvertTimeFromUtc会抛ArgumentException -
DateTime.Now是Local,但它的“本地”只对应运行机器的时区——部署到东京服务器的代码,Local就不是北京时间 - 从数据库读出的
datetime2值默认也是Unspecified,不手动设Kind就无法安全转换
TimeZoneInfo.FindSystemTimeZoneById 的 ID 怎么查?
Windows 和 Linux/macOS 的 ID 不一致,硬写字符串极易跨平台失败。正确做法是:
- 优先用标准 IANA 时区名(如
"Asia/Shanghai"),.NET 6+ 全平台支持,TimeZoneInfo.FindSystemTimeZoneById("Asia/Shanghai")可用 - 旧版本(.NET Framework / .NET Core 3.1)Windows 用
"China Standard Time",Linux 用"Asia/Shanghai"—— 必须运行时检测:RuntimeInformation.IsOSPlatform(OSPlatform.Linux) - 避免用
TimeZoneInfo.Local做转换依据,它可能被用户手动改过(比如把系统时区设成 Pacific Time 却处理中国业务)
ConvertTimeFromUtc 和 ConvertTimeToUtc 的参数陷阱
这两个方法看似对称,但行为差异很大:
-
ConvertTimeToUtc(DateTime dateTime, TimeZoneInfo timeZone):要求dateTime.Kind == DateTimeKind.Unspecified或Local;如果传入Utc类型,会直接返回原值(不报错但逻辑错) -
ConvertTimeFromUtc(DateTime dateTime, TimeZoneInfo timeZone):严格要求dateTime.Kind == DateTimeKind.Utc,否则抛异常 - 真实场景中,从 API 接收的 JSON 时间(如
"2024-06-01T12:00:00Z")反序列化后Kind是Utc,可直接进ConvertTimeFromUtc;但"2024-06-01T12:00:00"(无 Z)反序列化后是Unspecified,必须先用DateTime.SpecifyKind(..., DateTimeKind.Utc)显式标注
夏令时(DST)和历史时区变更怎么处理?
TimeZoneInfo 内置了 DST 规则和历史变更(如中国 1992 年起取消夏令时),但仅限于系统时区数据库已知的范围:
- Windows 更新时区库靠系统更新,老旧服务器可能缺失近年规则(如 2023 年巴西时区调整)
- Linux 依赖
tzdata包版本,Docker 镜像若没更新,"America/Sao_Paulo"可能按旧规则算偏移 - 关键业务(如金融结算)建议用
NodaTime库替代:它自带完整 IANA 数据库,版本可控,且ZonedDateTime类型天然防Kind混淆
最常被忽略的一点:数据库字段类型。用 datetime 存带时区的时间等于埋雷——SQL Server 的 datetimeoffset、PostgreSQL 的 timestamptz 才真正保留时区上下文;C# 层再强的 TimeZoneInfo 也救不回丢失的偏移量。


















