10.2 字符串格式

之前的章节介绍了用$前缀给字符串传递变量。本章讨论这个C# 6新功能背后的理论,并囊括格式化字符串提供的所有其他功能。

10.2.1 字符串插值

C# 6引入了给字符串使用$前缀的字符串插值。下面的示例使用$前缀创建了字符串s2,这个前缀允许在花括号中包含占位符来引用代码的结果。{ s1 }是字符串中的一个占位符,编译器将变量s1的值放在字符串s2中(代码文件StringFormats/Program. cs):

        string s1 = "World";
        string s2 = $"Hello, {s1}";

在现实中,这只是语法糖。对于带$前缀的字符串,编译器创建String.Format方法的调用。所以前面的代码段解读为:

        string s1 = "World";
        string s2 = String.Format("Hello, {0}", s1);

String.Format方法的第一个参数接受一个格式字符串,其中的占位符从0开始编号,其后是放入字符串空白处的参数。

新的字符串格式要方便得多,不需要编写那么多代码。

不仅可以使用变量来填写字符串的空白处,还可以使用返回一个值的任何方法:

         string s2 = $"Hello, {s1.ToUpper()}";

这段代码可解读为如下类似的语句:

        string s2 = String.Format("Hello, {0}", s1.ToUpper());

字符串还可以有多个空白处,如下所示的代码:

        int x = 3, y = 4;
        string s3 = $"The result of {x} + {y} is {x + y}";

解读为:

        string s3 = String.Format("The result of {0} and {1} is {2}", x, y, x + y);

1. FormattableString

把字符串赋予FormattableString,就很容易得到翻译过来的插值字符串。插值字符串可以直接分配,因为FormattableString比正常的字符串更适合匹配。这个类型定义了Format属性(返回得到的格式字符串)、ArgumentCount属性和方法GetArgument(返回值):

        int x = 3, y = 4;
        FormattableString s = $"The result of {x} + {y} is {x + y}";
        WriteLine($"format: {s.Format}");
        for (int i = 0; i < s.ArgumentCount; i++)
        {
          WriteLine($"argument {i}: {s.GetArgument(i)}");
        }

运行此代码段,输出结果如下:

        format: The result of {0} + {1} is {2}
        argument 0: 3
        argument 1: 4
        argument 2: 7

注意:类FormattableString在System名称空间中定义,但是需要.NET 4.6。如果想在.NET旧版本中使用FormattableString,可以自己创建这种类型,或使用NuGet包StringInterpolationBridge。

2.给字符串插值使用其他区域值

插值字符串默认使用当前的区域值,这很容易改变。辅助方法Invariant把插值字符串改为使用不变的区域值,而不是当前的区域值。因为插值字符串可以分配给FormattableString类型,所以它们可以传递给这个方法。FormattableString定义了允许传递IFormatProvider的ToString方法。接口IFormatProvider由CultureInfo类实现。把CultureInfo.InvariantCulture传递给IFormatProvider参数,就可把字符串改为使用不变的区域值:

        private string Invariant(FormattableString s) =>
          s.ToString(CultureInfo.InvariantCulture);

注意:第28章讨论了格式字符串的语言专有问题,以及区域值和不变的区域值。

在下面的代码段中,Invariant方法用来把一个字符串传递给第二个WriteLine方法。WriteLine的第一个调用使用当前的区域值,而第二个调用使用不变的区域值:

        var day = new DateTime(2025, 2, 14);
        WriteLine($"{day:d}");
        WriteLine(Invariant($"{day:d}"));

如果有英语区域值设置,结果就如下所示。如果系统配置了另一个区域值,第一个结果就是不同的。在任何情况下,都会看到不变区域值的差异:

        2/14/2025
        02/14/2015

使用不变的区域值,不需要自己实现方法,而可以直接使用FormattableString类的静态方法Invariant:

        WriteLine(FormattableString.Invariant($"{day:d}"));

3.转义花括号

如果希望在插值字符串中包括花括号,可以使用两个花括号转义它们:

        string s = "Hello";
        WriteLine($"{{s}} displays the value of s: {s}");

WriteLine方法被解读为如下实现代码:

        WriteLine(String.Format("{s} displays the value of s: {0}", s));

输出如下:

        {s} displays the value of s : Hello

还可以转义花括号,从格式字符串中建立一个新的格式字符串。下面看看这个代码段:

        string formatString = $"{s}, {{0}}";
        string s2 = "World";
        WriteLine(formatString, s2);

有了字符串变量formatString,编译器会把占位符0插入变量s,调用String.Format:

        string formatString = String.Format("{0}, {{0}}", s);

这会生成格式字符串,其中变量s替换为值Hello,删除第二个格式最外层的花括号:

        string formatString = "Hello, {0}";

在WriteLine方法的最后一行,使用变量s2的值把World字符串插值到新的占位符0中:

        WriteLine("Hello, World");

10.2.2 日期时间和数字的格式

除了给占位符使用字符串格式之外,还可以根据数据类型使用特定的格式。下面先从日期开始。在占位符中,格式字符串跟在表达式的后面,用冒号隔开。下面所示的例子是用于DateTime类型的D和d格式:

        var day = new DateTime(2025, 2, 14);
        WriteLine($"{day:D}");
        WriteLine($"{day:d}");

结果显示,用大写字母D表示长日期格式字符串,用小写字母d表示短日期字符串:

        Friday, February 14, 2025
        2/14/2025

根据所使用的大写或小写字符串,DateTime类型会得到不同的结果。根据系统的语言设置,输出可能不同。日期和时间是特定于语言的。

DateTime类型支持很多不同的标准格式字符串,显示日期和时间的所有表示:例如,t表示短时间格式,T表示长时间格式,g和G显示日期和时间。这里不讨论所有其他选项,在MSDN文档的DateTime类型的ToString方法中,可以找到相关介绍。

注意:应该提到的一个问题是,为DateTime构建自定义的格式字符串。自定义的日期和时间格式字符串可以结合格式说明符,例如dd-MMM-yyyy:

        WriteLine($"{day:dd-MMM-yyyy}");

结果如下:

        14-Feb-2025

这个自定义格式字符串利用dd把日期显示为两个数字(如果某个日期在10日之前,这就很重要,从这里可以看到d和dd之间的区别)、MMM(月份的缩写名称,注意它是大写,而mm表示分钟)和表示四位数年份的yyyy。同样,在MSDN文档中可以找到用于自定义日期和时间格式字符串的所有其他格式说明符。

数字的格式字符串不区分大小写。下面看看n、e、 x和c标准数字格式字符串:

        int i = 2477;
        WriteLine($"{i:n} {i:e} {i:x} {i:c}");

n格式字符串定义了一个数字格式,用组分隔符显示整数和小数。e表示使用指数表示法,x表示转换为十六进制,c显示货币:

        2,477.00 2.477000e+003 9ad $2,477.00

对于数字的表示,还可以使用定制的格式字符串。#格式说明符是一个数字占位符,如果数字可用,就显示数字;如果数字不可用,就不显示数字。0格式说明符是一个零占位符,显示相应的数字,如果数字不存在,就显示零。

        double d = 3.1415;
        WriteLine($"{d:###.###}");
        WriteLine($"{d:000.000}");

在示例代码中,对于double值,第一个结果把逗号后的值舍入为三位小数,第二个结果是显示逗号前的三个数字:

        3.142
        003.142

MSDN文档给百分比、往返和定点显示提供了所有的标准数字格式字符串,以及提供自定义格式字符串,用于使指数、小数点、组分隔符等显示不同的外观。

10.2.3 自定义字符串格式

格式字符串不限于内置类型,可以为自己的类型创建自定义格式字符串。为此,只需要实现接口IFormattable。

首先是一个简单的Person类,它包含FirstName和LastName属性(代码文件StringFormats/Person.cs):

        public class Person
        {
          public string FirstName { get; set; }
          public string LastName { get; set; }
        }

为了获得这个类的简单字符串表示,重写基类的ToString方法。这个方法返回由FirstName和LastName组成的字符串:

        public override string ToString() => FirstName + " " + LastName;

除了简单的字符串表示之外,Person类也应该支持格式字符串F,返回名L和姓A,后者代表“all”;并且应该提供与ToString方法相同的字符串表示。为实现自定义字符串,接口IFormattable定义了带两个参数的ToString方法:一个是格式的字符串参数,另一个是IFormatProvider参数。IFormatProvider参数未在示例代码中使用。可以基于区域值使用这个参数,进行不同的显示,因为CultureInfo类实现了该接口。

实现了这个接口的其他类是NumberFormatInfo和DateTimeFormatInfo。可以把实例传递到ToString方法的第二个参数,使用这些类配置数字和DateTime的字符串表示。ToString方法的实现代码只使用switch语句,基于格式字符串返回不同的字符串。为了使用格式字符串直接调用ToString方法,而不提供格式提供程序,应重载ToString方法。这个方法又调用有两个参数的ToString方法:

        public class Person : IFormattable
        {
          public string FirstName { get; set; }
          public string LastName { get; set; }
          public override string ToString() => FirstName + " " + LastName;
          public virtual string ToString(string format) => ToString(format, null);
          public string ToString(string format, IFormatProvider formatProvider)
          {
            switch (format)
            {
              case null:
              case "A":
                return ToString();
              case "F":
                return FirstName;
              case "L":
                return LastName;
              default:
                throw new FormatException($"invalid format string {format}");
            }
          }
        }

有了这些代码,就可以明确传递格式字符串,或隐式使用字符串插值,以调用ToString方法。隐式的调用使用带两个参数的ToString方法,并给IFormatProvider参数传递null(代码文件StringFormats/Program.cs):

        var p1 = new Person { FirstName = "Stephanie", LastName = "Nagel" };
        WriteLine(p1.ToString("F"));
        WriteLine($"{p1:F}");