Menu

C# DateTime 与 TimeSpan:Now、UtcNow、加减、比较与解析

在 C# 中处理日期和时间:创建 DateTime 值,Now、UtcNow 与 Today 的区别,加天数和月数,相减得到 TimeSpan,TotalHours 与 Hours 的区别,比较日期,DayOfWeek,用 ParseExact 和 TryParse 解析,DateTimeOffset 以及 DateOnly。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

System.DateTime 表示一个日期和一天中的时间,范围从公元 1 年到 9999 年,精度为 100 纳秒(一个 “tick”)。System.TimeSpan 表示一段时长,也就是两个 DateTime 值之间的差。两者都是不可变的值类型(结构体),所以每个操作都会返回一个新值。

输出:

2026-09-24 00:00:00
2026-09-24 14:30:00
14:30:05.250
2026 9 24
14:30
Thursday
267
2026-09-24 00:00
14:30:00

本页的每个示例都用显式的格式字符串打印日期。默认的 ToString() 遵循当前区域性(在美国是 9/24/2026 2:30:00 PM,在德国是 24.09.2026 14:30:00),所以它的输出取决于机器。格式代码见 DateTime 格式化。

无效的日期会抛出异常:new DateTime(2026, 2, 30) 会引发 ArgumentOutOfRangeException,月份写 13 或小时写 24 也一样。

Now、UtcNow 与 Today

有三个静态属性可以读取时钟:

示例输出:

Now:    2026-09-24 18:20:41 (Local)
UtcNow: 2026-09-24 16:20:41 (Utc)
Today:  2026-09-24 00:00:00

这个例子中本地时区比 UTC 早两个小时,所以前两行相差两小时;在设置为 UTC 的机器上,两者相同。Kind 属性记录一个值是 Local、Utc 还是 Unspecified(自己构造的日期默认是这个)。凡是要存储、记录日志、比较或发送给其他系统的时间,都用 DateTime.UtcNow:它不会在夏令时开始或结束时跳变,而且在每台服务器上都代表同一个时刻。只在把值显示给人看时才转换为本地时间。

要测量代码运行了多久,请使用 System.Diagnostics.Stopwatch,而不是把两个 DateTime.Now 相减;它的分辨率高得多,也不受时钟调整的影响。

加减时间

AddDays、AddHours、AddMinutes、AddSeconds、AddMonths 和 AddYears 返回一个新的 DateTime。传入负数就是往回推。因为 DateTime 是不可变的,所以必须把结果赋值给变量:

输出:

2026-01-31
2026-02-03 09:00
2026-01-30 21:00
2026-02-28
2027-01-31
10:30
29
True

当目标月份没有对应的日期时,AddMonths 会落到该月的最后一天:1 月 31 日加一个月是 2 月 28 日(闰年为 29 日),而不是 3 月 3 日。因此,连续两次加一个月和一次加两个月,得到的日期可能不同。

日期相减:TimeSpan

一个 DateTime 减去另一个,得到一个 TimeSpan:

输出:

3.20:30:00
Days: 3, Hours: 20, Minutes: 30
TotalDays: 3.85
TotalHours: 92.5
TotalMinutes: 5550
Nights: 4

这是 API 中最常被用错的部分。Days、Hours、Minutes 和 Seconds 是时长的分量(3 天、20 小时、30 分钟)。TotalDays、TotalHours 和 TotalMinutes 是用单一单位表示的整个时长,类型为 double。“客人住了多少小时?”的答案是 TotalHours(92.5),而不是 Hours(20)。

最后一行说明了一个相关问题:经过的时间是 3.85 天,但客人住了 4 晚。比较 .Date 部分计算的是日历天数,账单和“还有几天”这类显示通常要的正是它。

创建和格式化 TimeSpan 值

输出:

02:15:00
01:30:00
1.12:00:00
True
03:45:00
True
02:15
36h 0m
00:00:00

TimeSpan 支持 +、-、比较运算、Duration()(绝对值)和 Negate()。@"hh\:mm" 这样的自定义格式需要在字面字符前加反斜杠,而且其中的 hh 只显示小时分量(0 至 23),所以超过一天的时长,要像倒数第二行那样用 TotalHours 拼出文本。

比较日期

DateTime 支持 ==、!=、<、>、<= 和 >=,还有 CompareTo 和 DateTime.Compare。如果只比较日期而忽略时间,就比较 .Date 属性:

输出:

True
True
1
True
2026-09-01

要判断“这个时间戳是否在 9 月 30 日之内?”,应该像上面那样,用 < 与下一天的开始时间比较。写成 check <= end 会把最后一天午夜之后的时间全部排除在外,因为 end 是 2026-09-30 00:00:00。

比较只看 tick,不看 Kind:一个 Local 值和一个 Utc 值只要打印出来一样,就被视为相等,尽管它们是不同的时刻。这也是存储时间要用 UTC 的另一个理由。

星期几与一周的开始

DayOfWeek 是一个枚举,从 Sunday(0)到 Saturday(6)。对它做算术运算,可以找出工作日和一周的边界:

输出:

Thursday
4
Weekend: False
Week starts 2026-09-21 (Monday)
Next Friday: 2026-09-25
2026-09-01 to 2026-09-30

DayOfWeek.ToString() 打印的星期名称永远是英文。需要本地化的名称时,用 "dddd" 加上区域性来格式化日期。

从字符串解析日期

知道输入的格式时,用 ParseExact 或 TryParseExact 并配合 CultureInfo.InvariantCulture。格式字符串使用的代码和格式化时相同:

输出:

2026-09-24 00:00
2026-09-24 18:05
'2026-02-28' -> Saturday, February 28
'2026-02-30' -> invalid
'28.02.2026' -> invalid
'' -> invalid
2026-02-28
2026-09-24 10:00 Utc

不带格式的 DateTime.Parse(text) 会尝试用当前区域性去猜。"03/04/2026" 在美国的机器上是 3 月 4 日,在英国的机器上是 4 月 3 日,在你笔记本上能解析的日期,到了服务器上可能抛出 FormatException。Parse 只留给本地用户手动输入的内容;文件、API 和数据库用 ParseExact 加固定区域性。文本不匹配时,ParseExact 抛出 FormatException;TryParseExact 则返回 false。

计算年龄

把出生日期相减再除以 365,在生日前后和闰年时都会算错。正确做法是比较年份,如果今年的生日还没到,再减一:

输出:

36
35
18
70 days to go

DateTimeOffset

除了含糊的 Kind 标志,DateTime 并不记录自己属于哪个时区。DateTimeOffset 把值和它相对 UTC 的偏移量一起保存,所以它总是标识一个确切的时刻:

输出:

2026-09-24 14:00 +02:00
2026-09-24 12:00
2026-09-24 12:30
00:30:00
21:00 +09:00

时间戳请使用 DateTimeOffset(或 UTC 的 DateTime 值):下单时间、消息发送时间都属于这一类。数据库和 JSON 序列化器都能很好地处理它。要在带夏令时规则的命名时区之间转换,用 TimeZoneInfo.ConvertTime;在旧版 .NET 上,时区 ID 因操作系统而异(Linux 上是 "Europe/Paris",Windows 上是 "Romance Standard Time"),.NET 6 及更高版本两种都接受。

DateOnly 与 TimeOnly(.NET 6 及更高版本)

很多值只是没有时间的日期(生日、截止日期),或者没有日期的时间(营业时间)。.NET 6 为它们新增了两个类型:

// .NET 6 and later
DateOnly birthday = new DateOnly(1990, 9, 24);
DateOnly due = DateOnly.FromDateTime(DateTime.Today).AddDays(14);
int daysLeft = due.DayNumber - DateOnly.FromDateTime(DateTime.Today).DayNumber;

TimeOnly opens = new TimeOnly(9, 0);
TimeOnly closes = new TimeOnly(17, 30);
bool isOpen = TimeOnly.FromDateTime(DateTime.Now).IsBetween(opens, closes);

它们消除了一类 bug:多出来的时间或时区让日期偏移一天。较旧的代码,以及面向 .NET Framework 或 Unity 的代码,仍然使用把时间留在午夜的 DateTime。

常见错误

  • 丢弃 AddDays 的结果。 DateTime 是不可变的;要把返回值赋值回去。
  • 用 Hours 代替 TotalHours。 分量与总时长是两回事。
  • 存储 DateTime.Now。 存储 UTC,显示时再转换。
  • 调用 ToString() 时不传格式,用在日志、文件或测试中,输出就会取决于机器的区域性。
  • 用 DateTime.Parse 解析机器生成的数据。 请用 ParseExact 加固定区域性。
  • 在格式字符串中混淆 mm 和 MM(分钟和月份)。见 DateTime 格式化。

常见问题

DateTime.Now 和 DateTime.UtcNow 有什么区别?

DateTime.Now 是计算机本地时区的当前时间,Kind 为 Local。DateTime.UtcNow 是 UTC 的当前时间,Kind 为 Utc,而且因为省去了时区转换,它也更快。存储和比较时间戳时用 UTC,只在显示时才转换为本地时间。

C# 中如何计算两个日期之间的差?

直接相减:TimeSpan gap = end - start;。然后读取 gap.TotalDays、gap.TotalHours 或 gap.TotalMinutes,以 double 形式得到整个时长,或者读取 gap.Days 得到整天的部分。日历意义上的月数或年数没有现成的属性,因为每个月的长度不同;需要自己比较年份和月份字段。

TimeSpan.Hours 和 TotalHours 有什么区别?

Hours 只是去掉整天之后剩下的小时分量,范围是 0 至 23。TotalHours 是用小时表示的整个时长,类型为 double。对于 1 天 3 小时的时长,Hours 是 3,TotalHours 是 27。本该用 TotalHours 的地方用了 Hours,是非常常见的 bug。

C# 中如何解析日期字符串?

知道格式时,用 DateTime.ParseExact(text, "yyyy-MM-dd", CultureInfo.InvariantCulture),或者用 DateTime.TryParseExact,这样输入有误时得到的是 false,而不是 FormatException。DateTime.Parse 会根据当前区域性猜测格式,所以 03/04/2026 在美国是 3 月 4 日,在英国却是 4 月 3 日。

为什么 AddDays 没有改变我的 DateTime?

DateTime 是不可变的值类型。AddDays、AddHours 等方法会返回一个新的 DateTime,原来的值保持不变,所以必须把结果赋值回去:due = due.AddDays(7);。

什么时候应该用 DateTimeOffset 而不是 DateTime?

需要标识一个确切时刻的时间戳,比如下单时间或日志写入时间,就用 DateTimeOffset,尤其是数据会在服务器和时区之间流转的时候。它把相对 UTC 的偏移量和值一起保存。只用 UTC 的时间戳,以及时区没有意义的日期,用 DateTime 就够了。

Coddy 编程语言插图

和 Coddy 一起学编程

开始