timeonly仅表示一天中的时刻,适用于会议时间、闹钟、营业时段等场景;不支持时区、日期及跨日计算,错误用于带时区或跨午夜场景会引发隐蔽bug。

TimeOnly 不是 DateTime 的简化版,它没有时区、没有日期、不能直接参与跨日计算——用错场景会出隐蔽 bug。
TimeOnly 适合什么场景
它只表示「一天中的某个时刻」,比如会议开始时间、闹钟设定、营业时段(09:00–17:30)、排班表里的班次时间。它不关心这是哪天、在哪个时区、有没有夏令时。
- ✅ 正确用法:存储用户设置的“每日提醒时间”
TimeOnly reminder = new TimeOnly(8, 0); - ✅ 正确用法:判断当前时间是否在营业范围内:
currentTime.IsBetween(openTime, closeTime) - ❌ 错误用法:试图用它表示“北京时间 23:00 的直播”,因为没日期+没时区,无法转成 UTC 或其他时区时间
- ❌ 错误用法:做跨午夜计算,比如
23:00 + 3 小时→ 这得靠TimeSpan配合DateTime或手动处理边界
从 DateTime 提取 TimeOnly 要小心默认行为
TimeOnly.FromDateTime(DateTime) 只取时间部分,完全忽略 DateTime.Kind 和时区信息。如果你传入的是 DateTimeKind.Utc 或 DateTimeKind.Local,它不会做任何转换,直接截取小时/分钟/秒。
- 常见错误:前端传来
"2026-05-15T14:30:00Z",后端解析为DateTime.UtcNow,再调用TimeOnly.FromDateTime()→ 得到的是 14:30,但用户本意可能是“本地时间 14:30” - 正确做法:先确认原始时间的语义。如果是“用户本地时间”,应统一转成
DateTimeKind.Unspecified再提取;如果是“服务端统一时间”,需明确文档说明该TimeOnly始终对应 UTC 时间 - 安全写法示例:
DateTime dt = DateTime.SpecifyKind(DateTime.Parse("2026-05-15T14:30:00"), DateTimeKind.Unspecified); TimeOnly t = TimeOnly.FromDateTime(dt); // 明确剥离时区意图
格式化与解析要注意 Culture 和分隔符
TimeOnly.ToString() 默认使用当前线程 CultureInfo,可能输出 "14:30:00"(en-US)或 "14.30.00"(de-DE)。解析字符串时更危险:不指定 culture 容易因系统区域设置不同而失败。
- 推荐显式指定 culture:
t.ToString("HH:mm", CultureInfo.InvariantCulture) - 解析务必用
TryParse并传 culture:if (TimeOnly.TryParse("14:30", CultureInfo.GetCultureInfo("en-US"), out var result)) { ... } - 避免直接用
Parse("14:30")—— 在 fr-FR 系统下可能抛FormatException - 注意:标准格式符
"t"(短时间)和"T"(长时间)对TimeOnly有效,但"g"、"G"等含日期的格式符会报错
IsBetween 处理跨午夜逻辑有隐含规则
TimeOnly.IsBetween(start, end) 支持 23:00 到 01:00 这类跨日范围,但它内部是按“数值循环”判断的:若 start > end,就自动视为跨午夜。
- 例如:
new TimeOnly(0, 30).IsBetween(new TimeOnly(23, 0), new TimeOnly(1, 0))返回true - 但注意:end 是独占的(exclusive),所以
01:00本身不算在范围内 - 陷阱:如果 start == end,方法直接返回
false,哪怕你本意是“全天开放”。这时得单独判断:if (start == end) { /* 全天逻辑 */ } else { t.IsBetween(start, end); } - 别依赖它做“持续时间”计算——它不返回毫秒差,只返回布尔值
最易被忽略的一点:TimeOnly 的底层是 int(以刻度数记一天内的时间),但它不暴露这个字段;所有加减操作都必须通过 TimeSpan 中转,且结果仍受限于 00:00–23:59:59.9999999 范围。超出会抛 ArgumentOutOfRangeException,而不是自动归零或进位。










