在 C#/.NET 中,格式化字符串的机制多种多样:从简单地拼接对象,到调用重写的object.ToString()方法,再到使用 ICustomFormatter(本练习不涉及)。
最常见的两种字符串格式化机制是字符串内插和 String.Format()。如果涉及多行之类的复杂情况,也可以用 StringBuilder (cross-ref-tba) 类来逐步构建字符串。
ToString()
所有类和结构体都继承自System.Object(),它有一个ToString()方法。例如,new DateTime(2019, 5, 23).ToString()在采用美国区域性的线程上会呈现“05/23/2019 00:00:00”(见下文)。在字符串拼接等情况下,可能会隐式调用这个默认的ToString()方法,比如"" + new DateTime(2019, 5, 23)会得到同样的结果。
除了默认的ToString()方法之外,格式化会成为问题的类型还会有接收格式字符串,甚至格式提供程序的重载。在 BCL(基类库)中,这类类型主要有数字、日期、枚举和 GUID。
String.Format()接收一个字符串(文档中称为_复合格式_),它由固定文本和占位符(文档中称为_格式项_)组成,此外还接收数量可变的实参。返回值会用对应的实参解析每个_格式项_,再把解析出的值与固定文本组合起来。
string.Format("I had {0} bitcoins on {1}, the day I forgot my password.", 55.5, new DateTime(2010, 2, 25))
// => "I had 55.5 bitcoins on 2/25/2010 00:00:00, the day I forgot my password." - US settings
如果这个格式要在多个表达式中当作模板来用,或者把格式和待格式化的表达式写在一起太麻烦(比如涉及[逐字字符串][verbatim-strings]时),那么Format()可能比内插更合适。
这种机制在技术上称为复合格式。
更完整的、利用了_复合格式_的字符串生成方法列表见这篇文章。
内插字符串以$为前缀,并把运行时表达式放在大括号里。格式项的语法如下:$"{<interpolationExpression>[,<alignment>][:<formatString>]}"。它省去了单独列出实参的麻烦,结果在功能上等价于String.Format()机制。
表达式可以是作用域中的任何内容。_对齐_是指文本所在“字段”的长度。如果_对齐_为正,文本左侧用空格填充;如果为负,则在文本右侧填充。
var loadsOf = 55.5;
var thatDay = new DateTime(2010, 2, 25);
$"I had {loadsOf} bitcoins on {thatDay}, the day I forgot my password."
// => "I had 55.5 bitcoins on 2/25/2010 00:00:00, the day I forgot my password." - US settings
大括号里的内容,在复合格式中是占位符,在字符串内插中是内插表达式,都称为_格式项_。
一个格式项最多可以由 3 部分组成。第一部分是必需的表达式或实参占位符,如上面的示例代码所示。此外还有可选的对齐(以逗号“,”引入)和可选的_格式字符串_(以冒号“:”引入)。
{<interpolationExpression>[,<alignment>][:<formatString>]}
_对齐_指定文本所在“字段”的长度:对齐为正时左侧用空格填充,为负时右侧填充。
_格式字符串_指定文本输出的形式,比如数字是否要包含千位分隔符,或者DateTime对象是否只输出日期部分。
下面的代码演示了如何显示DateTime对象的日期部分,以及如何以指数形式显示浮点数。
var loadsOf = 55.5;
var thatDay = new DateTime(2010, 2, 25);
$"I had {loadsOf:E} bitcoins on {thatDay:d}, the day I forgot my password."
// => I had 5.550000E+001 bitcoins on 02/25/2010, the day I forgot my password. - US settings
string.Format(
"I had {0:E} bitcoins on {1:d}, the day I forgot my password.",
loadsOf, thatDay)
// => I had 5.550000E+001 bitcoins on 02/25/2010, the day I forgot my password. - US settings
数字和日期都有标准格式化和自定义格式化。_自定义_和_标准_之间没有本质区别,只是你有机会用格式字符组合出自定义格式字符串。这里的“自定义”与开发自己的自定义格式化程序时用到的 ICustomFormatter 接口没有任何关系。
基类库(BCL)提供 2 个格式化程序:DateTimeFormatInfo和NumberFormatInfo,以及 6 组格式字符串。
下面是各组_格式字符串_的列表:
Enum和GUID的_格式字符串_可以归为_标准_。虽然enum和GUID有接收IFormatProvider的ToString()重载,但不太清楚它到底起什么作用。
库中试图给_格式字符串_注入一些一致性(除了它们都以字符串表示这一点之外)。这种对一致性的追求体现在标准字符串里。实际上,作为开发者,你很少会在意标准字符串和自定义字符串之间的区别。如果你的类看起来需要,那么为自己的类实现格式化程序时参照现有的标准字符串是个好主意;除此之外,你大可以忽略这一区别。
每个执行线程都有一个默认区域性Thread.CurrentThread.CurrentCulture,它封装在CultureInfo的实例中。线程的区域性决定了日期和数字默认如何格式化,并会考虑地区差异,比如英国习惯的日期格式_DD/MM/YYYY_和美国习惯的_MM/DD/YYYY_之间的差别。
CultureInfo实现了IFormatProvider接口,该接口可以传给String.Format()的某些重载。用它就能覆盖线程的区域性。