.NET DateTime 格式化

126 阅读13分钟

在软件开发中,将 DateTime 对象转换为特定格式的字符串是一项基础且至关重要的技能。这个过程不仅仅是“显示”时间,更关乎数据的精确性、可读性、可移植性以及全球化支持。本指南将从最基础的概念讲起,逐步深入到高级主题与常见陷阱,助您完全掌握 C# 中的时间处理。


引言:为什么需要格式化?

想象一下,你有一个原始的时间数据,比如“2025年9月22日晚上10点38分29秒”。计算机内部存储的是一个标准值,但你希望它在不同场景下以不同的样子显示出来:

  • 2025-09-22

  • 22/09/2025

  • 2025年09月22日 22:38

  • 20250922223829 (常用作文件名或ID)

计算机不知道你喜欢哪种样子,所以你需要给它一套“暗号”,告诉它如何排列组合“年、月、日、时、分、秒”。这套“暗号”就是格式字符串(Format String)。在 C# 中,我们通过 DateTime.Now.ToString("暗号") 来实现这个过程。


第一部分:自定义日期和时间格式字符串

自定义格式字符串为你提供了像素级的控制能力,让你能精确地构建任何你想要的输出。核心在于理解每一个“格式说明符”(Format Specifier)的精确含义,包括它的大小写和重复次数。

1.1 核心说明符详解表

下表详细列出了最常用的说明符,并解释了它们的长度和大小写差异。

分类说明符名称/含义详细解释与示例 (基于 2025-09-08 07:05:03.123)
年 (Year)y年份,1-2位25
yy年份,2位25
yyyy年份,4位2025
yyyyy年份,5位 (不足则前面补0)02025
月 (Month)M月份,1-2位9 (九月)
MM月份,2位(不足则前面补0) 09
MMM月份的缩写名称Sep (取决于区域设置,中文可能是“九月”)
MMMM月份的完整名称September(取决于区域设置,中文可能是“九月”)
日 (Day)d日期,1-2位8
dd日期,2位 (不足则前面补0)09
ddd星期的缩写名称Mon (周一)
dddd星期的完整名称Monday (星期一)
时 (Hour)h12小时制,1-2位7 (上午7点)
hh12小时制,2位07
H24小时制,1-2位7
HH24小时制,2位07
分 (Minute)m分钟,1-2位5
mm分钟,2位05
秒 (Second)f秒,1-2位3
ss秒,2位03
秒的小数f秒的十分之一1
ff秒的百分之一12
fff毫秒 (3位)123
fffffff全部7位精度 (Tick)1230000
F, FF..显示有意义的位数,尾部0不显示F -> 1, FF -> 12, FFF -> 123
AM/PMtAM/PM 缩写A (上午)
ttAM/PM 完整AM
时区zUTC 偏移 (小时)+9 (假设在东京)
zzUTC 偏移 (小时, 补0)+09
zzzUTC 偏移 (小时:分钟)+09:00
K时区信息 (Kind属性)+09:00 (Local), Z (Utc), "" (Unspecified)

⭐ 小白第一大坑:MM vs mm

  • 大写的 MM 是 Month (月份)

  • 小写的 mm 是 minute (分钟)

这是最常见且后果最严重的错误,必须牢记!

1.2 转义字符与原义字符

  • 原义字符 (Literal Characters): 任何未在以上表格中定义的字符(如 -, /, , 年, 月, 日)都会被直接复制到输出字符串中。

  • 转义: 如果你想输出一个本身就是格式说明符的字符,比如 h,你需要用单引号 ' ' 将其包裹,或用反斜杠 \ 进行转义。

    • 示例: DateTime.Now.ToString(@"HH'h'mm'm'ss's'") -> 输出 07h05m03s

第二部分:标准日期和时间格式字符串

标准格式是微软预定义好的一套“快捷方式”。它们通常是单个字符,其代表的具体格式会根据当前的“区域性文化(Culture)”而改变。这使得你的程序更容易做全球化。

说明符名称示例输出 (en-US)示例输出 (zh-CN)
d短日期9/8/20252025/9/8
D长日期Monday, September 8, 20252025年9月8日
t短时间7:05 AM7:05
T长时间7:05:03 AM7:05:03
f完整日期/短时间Monday, September 8, 2025 7:05 AM2025年9月8日 7:05
F完整日期/长时间Monday, September 8, 2025 7:05:03 AM2025年9月8日 7:05:03
g常规日期/短时间9/8/2025 7:05 AM2025/9/8 7:05
G常规日期/长时间9/8/2025 7:05:03 AM2025/9/8 7:05:03
M, m月/日September 89月8日
Y, y年/月September 20252025年9月
o返(Round-trip)2025-09-08T07:05:03.1230000+09:00(同左, 不变)
u通用可排序2025-09-08 07:05:03Z (UTC时间)(同左, 不变)

⭐ 最佳实践:

  • 用于数据交换/存储/API: 强烈推荐使用 o (ISO 8601 格式)。它包含了完整的日期、时间、毫秒精度和时区信息,是目前最通用、最无歧义的格式,可以被任何现代编程语言轻松解析。

  • 用于向用户显示: 使用标准格式 (D, F 等) 是一个好起点,因为它会自动适应用户的系统区域设置。

第三部分:实战详解

3.1 它们是什么?(一个简单的比喻)

想象一下,自定义格式字符串 (yyyy-MM-dd...) 就像是相机的“专业/手动模式”。你需要自己手动调整光圈、快门、ISO等所有参数,控制力极强,但也很复杂。

而标准格式字符串 (d, D, f, F 等) 就像是相机的“预设模式”,比如“人像模式”、“风景模式”、“夜景模式”。

你不需要关心内部的复杂参数。

你只需要告诉相机你的意图(我想拍人像)。

相机就会自动为你应用一套最佳的参数组合。

标准格式字符串也是如此。你只需告诉 .NET 你的显示意图(我想显示一个“长日期”),.NET 就会自动为你选择一套最符合当前用户系统区域与语言习惯的格式。

3.2 我该怎么用?(语法和代码示例)

用法和自定义格式完全一样,就是把那个长长的自定义字符串,换成一个单独的字母。

就是这么简单!

来看代码,假设当前时间是 2025年9月22日 晚上11:20:59:

DateTime now = DateTime.Now;

// 以前,用自定义格式,你需要自己拼凑:
string customLongDate = now.ToString("yyyy年M月d日");
Console.WriteLine($"自定义长日期: {customLongDate}"); 
// 输出: 自定义长日期: 2025年9月22日

// 现在,用标准格式,你只需告诉它你的“意图”:
string standardLongDate = now.ToString("D"); // D 的意图就是“长日期格式”
Console.WriteLine($"标准长日期 (D): {standardLongDate}");
// 输出: 标准长日期 (D): 2025年9月22日

string standardShortDate = now.ToString("d"); // d 的意图是“短日期格式”
Console.WriteLine($"标准短日期 (d): {standardShortDate}");
// 输出: 标准短日期 (d): 2025/9/22

string standardFullDateTime = now.ToString("F"); // F 的意图是“完整日期和长时间格式”
Console.WriteLine($"标准完整日期时间 (F): {standardFullDateTime}");
// 输出: 标准完整日期时间 (F): 2025年9月22日 23:20:59

string standardGeneralDateTime = now.ToString("g"); // g 的意图是“常规日期和短时间格式”
Console.WriteLine($"标准常规日期时间 (g): {standardGeneralDateTime}");
// 输出: 标准常规日期时间 (g): 2025/9/22 23:20

你看到了吗?你不再需要记住 yyyy 还是 dd,你只需要记住几个代表“意图”的字母即可。

3.3 它们为什么如此强大?(全球化的威力)

标准格式的真正威力在于,同一份代码,在不同国家用户的电脑上,会自动显示出最符合他们习惯的格式。

场景: 假设你开发了一款软件,要卖给中国、美国、德国的用户。

如果你的代码是硬编码的自定义格式: now.ToString("yyyy-MM-dd");

  • 中国用户看到 2025-09-22,觉得还行。

  • 美国用户看到 2025-09-22,也觉得还行,但他们更习惯 9/22/2025。

  • 德国用户看到 2025-09-22,也觉得还行,但他们更习惯 22.09.2025。

现在,如果我们只用一个标准格式字母 d,看看会发生什么。下面的代码模拟了在不同国家环境下运行你的程序:

using System.Globalization;

DateTime now = new DateTime(2025, 9, 22, 23, 20, 59);

// 1. 在中国用户的电脑上运行时...
Thread.CurrentThread.CurrentCulture = new CultureInfo("zh-CN");
Console.WriteLine($"在中国 (zh-CN), 'd' 格式显示为: {now.ToString("d")}");
Console.WriteLine($"在中国 (zh-CN), 'D' 格式显示为: {now.ToString("D")}");

Console.WriteLine("---");

// 2. 在美国用户的电脑上运行时...
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
Console.WriteLine($"在美国 (en-US), 'd' 格式显示为: {now.ToString("d")}");
Console.WriteLine($"在美国 (en-US), 'D' 格式显示为: {now.ToString("D")}");

Console.WriteLine("---");

// 3. 在德国用户的电脑上运行时...
Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE");
Console.WriteLine($"在德国 (de-DE), 'd' 格式显示为: {now.ToString("d")}");
Console.WriteLine($"在德国 (de-DE), 'D' 格式显示为: {now.ToString("D")}");

上述代码的输出结果:

在中国 (zh-CN), 'd' 格式显示为: 2025/9/22
在中国 (zh-CN), 'D' 格式显示为: 2025年9月22日
---
在美国 (en-US), 'd' 格式显示为: 9/22/2025
在美国 (en-US), 'D' 格式显示为: Monday, September 22, 2025
---
在德国 (de-DE), 'd' 格式显示为: 22.09.2025
在德国 (de-DE), 'D' 格式显示为: Montag, 22. September 2025

结论: 你只写了一行业务代码 now.ToString("d"),却完美地适配了三个国家用户的本地习惯!这就是标准格式的魔力,它让你的程序具备了“入乡随俗”的全球化能力。

3.4 特殊的“不变”格式:o 和 u

在所有标准格式中,o 和 u 是两个特殊的存在。它们不用于显示给用户,而是用于机器之间的数据交换(比如API通信、存入数据库、写入日志文件)。

它们的特点是固定不变,不受任何区域文化影响,从而保证数据在任何系统上都能被精确、无歧义地解析。

DateTimeOffset now = DateTimeOffset.Now;

// "o" 格式 (ISO 8601),信息最全,强烈推荐
string forApi = now.ToString("o"); 
Console.WriteLine(forApi);
// 输出: 2025-09-22T23:20:59.1234567+09:00 (包含了毫秒和时区偏移)

// "u" 格式,可排序,适合用作文件名或ID
string forFileName = DateTime.UtcNow.ToString("u");
Console.WriteLine(forFileName);
// 输出: 2025-09-22 14:20:59Z (注意这是UTC时间,以Z结尾)

总结一下使用场景 当你要把时间显示给用户时:优先使用标准格式 d, D, f, F, g, G, t, T 等,让你的程序自动适应用户的文化习惯。

当你要存储时间、或在程序间传递时间数据时:必须使用不受文化影响的格式,首选 o,其次是 u 或者一个固定的自定义格式(如 yyyy-MM-dd HH:mm:ss.fff)。

第四部分:常见错误与现象剖析

致命错误:大小写混用的后果

在 .NET 的 DateTime 格式化中,大小写是严格区分的。将一个格式说明符的大小写搞混,并非“样式”上的小问题,而是根本性地改变了它的含义。

我们以当前时间 2025年9月22日 22:38:29 为例,看看具体会发生什么:

错误的写法 (Incorrect Code)开发者预期的结果实际的灾难性输出现象与原因分析
ToString("yyyy-mm-dd")2025-09-222025-38-22【最经典的错误】 你想用 MM (月份),却写成了 mm (分钟)。于是,系统把当前的分钟数 (38) 插入到了月份的位置,生成了一个无效的日期字符串。
ToString("HH:MM:ss")22:38:2922:09:29【反向错误】 你想用 mm (分钟),却写成了 MM (月份)。于是,系统把当前的月份 (09) 插入到了分钟的位置。这会导致时间记录出现巨大偏差。
ToString("yyyy-MM-DD")2025-09-22程序崩溃 (FormatException)【直接异常】 很多其他编程语言用 DD 表示日期,但在.NET中,表示“天”的只能是小写的 d 或 dd。大写的 DD 是一个无效的格式说明符,因此程序会直接抛出 FormatException 异常并终止运行。
ToString("hh:mm:ss") (在晚上10点使用)22:38:2910:38:29【逻辑错误】 你想用24小时制 HH,却写成了12小时制 hh。这不会报错,但会丢失 AM/PM 的上下文,导致下午的时间被错误地记录为上午。在日志和数据跟踪中,这是非常危险的。
ToString("D") (小写的标准格式)Monday, September 22, 202509/22/2025 (取决于文化)【标准格式混淆】 标准格式也是大小写敏感的。大写 D 代表“长日期格式”,而小写 d 代表“短日期格式”。它们输出截然不同。

第五部分:进阶与避坑指南

除了大小写混用,以下几点是在实际开发中极其容易出错的地方,请务必留意:

陷阱一:区域性文化 (CultureInfo) 的“隐形杀手”

  • 问题描述: 同样的代码在你的电脑上运行正常,部署到服务器上就出错了。或者,美国用户反馈正常,欧洲用户反馈日期解析失败。

  • 原因: 日期格式在不同国家地区有不同习惯。例如,01/02/2025 在美国 (en-US) 表示1月2日,但在英国 (en-GB) 表示2月1日。

  • 避坑策略:

    1. 数据交换时: 坚持使用不受区域性影响的格式。首选 o (ISO 8601) 格式,其次是指定 CultureInfo.InvariantCulture(不变区域性)。

    2. 解析时: 绝对不要用 DateTime.Parse() 去解析来源不明确的字符串。请使用 DateTime.ParseExact() 或 DateTime.TryParseExact(),并明确提供字符串所对应的格式和 CultureInfo。

using System.Globalization;

// 安全的做法
string fromApi = "2025-09-22T22:38:29.123Z"; // ISO 8601 格式
DateTime dt = DateTime.Parse(fromApi, null, DateTimeStyles.RoundtripKind);

string fromEuroSource = "22/09/2025";
DateTime dtEuro = DateTime.ParseExact(fromEuroSource, "dd/MM/yyyy", CultureInfo.InvariantCulture);

陷阱二:时区 (Time Zone) 的“时空错乱”

  • 问题描述: 数据库里存的时间看起来都对,但业务逻辑一算就发现时间点不对,尤其在跨国应用或夏令时切换时。

  • 原因: DateTime.Now 获取的是服务器本地时间。如果你的服务器部署在中国,用户在美国,记录下的时间戳是北京时间,这与用户的实际操作时间有很大偏差。

  • 避坑策略:

    1. 时间戳记录: 永远使用 DateTime.UtcNow 来记录一个事件发生的绝对时间点。UTC是世界标准时间,不含时区和夏令时,是所有时区计算的基准。

    2. 需要时区信息的场景: 如果你的业务需要精确记录用户所在时区的时间(例如预定会议),请使用 DateTimeOffset 类型。它同时包含了UTC时间和相对于UTC的偏移量,信息更完整。

    3. 转换显示: 从数据库取出UTC时间后,根据用户的时区设置,将其转换为用户本地时间再显示给用户。

陷阱三:字符串解析的可靠性 (Parse vs. ParseExact)

  • 问题描述: 使用 Parse() 解析一个看起来没问题的日期字符串,偶尔会得到一个完全错误的时间,且不报错。

  • 原因: Parse() 会“非常努力”地去尝试多种它所知道的格式,有时会导致意外的匹配。

  • 避坑策略: 建立一个原则:只要你知道输入字符串的格式,就必须使用 ParseExact() 或 TryParseExact()。这让你的代码意图清晰,行为可预测且健壮。

陷阱四:对毫秒精度的误解

  • 问题描述: 使用 "yyyy-MM-dd HH:mm:ss.f" 想要获取毫秒,但发现精度丢失。

  • 原因: 单个 f 只代表秒的十分位(100毫秒)。你需要使用 fff 来代表完整的毫秒(3位)。如果你需要更高精度(微秒等),可以使用更多的 f (最多7个)。

  • 避坑策略: 记住 fff 是毫秒的常用格式。如果需要和数据库或API进行高精度时间交互,请检查对方要求的精度位数。

第六部分:官方文档链接

自定义日期和时间格式字符串 (Custom date and time format strings): learn.microsoft.com/zh-cn/dotne…

标准日期和时间格式字符串 (Standard date and time format strings): learn.microsoft.com/zh-cn/dotne…