NET 中的自定义字符串格式设置






4.80/5 (52投票s)
讨论了现有类型的自定义格式提供程序的实现以及用户定义类型的自定义格式设置。
引言
在许多应用程序中,格式化字符串输出是很难避免的,即使在使用现代图形用户界面时也是如此。不可避免的是,在某个时候您需要以易于理解的方式格式化数据。几乎每种运行时都有其字符串格式化过程集,.NET Framework 也不例外。了解 .NET Framework 中如何实现感知区域性且可扩展的字符串格式设置,可以帮助您创建更好的数据格式化方式。
本文与其说是讨论 .NET Framework 类库中存在的格式提供程序(尽管会简要讨论),不如说是关于如何编写自定义格式例程,使其能够轻松地在需要的地方使用。文章将讨论基本的字符串格式设置,.NET Framework 中的格式提供程序,使用自定义格式提供程序扩展现有类型,以及使用自定义格式设置扩展您的类型。
字符串格式化变得简单
在任何语言中,都有无数种格式化字符串的方法。它可以简单地将所有数据转换为字符串并串联起来,或者使用带有格式说明符的内联表达式。
以 Perl 为例。这个简单的示例在其 print
函数中使用了本地字符串变量,以及一个实数,该实数将根据 LC_CTYPE
环境变量使用适当的小数分隔符。
use locale;
my $var1 = "Perl";
my $var2 = 5.6;
print("Hello, from $var1 $var2!");
print
函数只是简单地对表达式中的变量求值,并将其写入 STDOUT
(标准输出句柄)。当前区域性几乎不影响格式,而无需先将变量传递给其他函数。这些内联表达式也不允许开发人员在评估相同变量的同时使用不同的表达式。
ANSI C 的 printf
函数通过使用可变参数列表和带索引的格式说明符将字符串表达式与要格式化的变量分离开来,从而解决了这个问题。
char* var1 = "ANSI C";
float var2 = 7.1;
printf("Hello, from %s %g!", var1, var2);
printf
函数同样受 Perl 所属的区域性环境变量的影响。这个 ANSI C 示例显示了如何使用一个单独的字符串来格式化变量参数列表。这有助于加载不同的格式字符串,包括不同语言的格式字符串。格式说明符此外还让您可以更精确地控制变量的求值方式,例如以十六进制表示法打印整数,尽管这些格式说明符非常不灵活。
在上面两个示例中,使用标准运行时库进行字符串格式化相当不灵活。还存在其他解决方案,它们使用第三方库或用户定义的函数在求值参数之前对其进行格式化,但如果运行时能够提供可扩展的格式设置解决方案,而无需支持第三方代码或在格式化参数之前调用用户定义的函数,那将是很好的。
在 .NET Framework 中,有几种方法可以格式化内联字符串表达式,包括 String.Format
[^]、StringBuilder.AppendFormat
[^] 和 TextWriter.Write
[^](以及 WriteLine
[^]),Console
[^] 类继承了这些方法。这些方法不仅允许您加载不同的字符串表达式进行格式化,还允许您使用 Framework 类库 (FCL) 提供的各种格式说明符以及自定义格式说明符(稍后将介绍)来控制变量列表的格式。下面的示例使用 Thread.CurrentCulture
中的 CultureInfo
来格式化相关变量。
string var1 = ".NET";
float var2 = 1.1;
string.Format("Hello, from {0} {1}!", var1, var2);
还要注意,格式说明符也是带索引的,这意味着您可以根据变量参数列表中的顺序来评估参数。您甚至可以重复使用同一变量。这看起来也更美观,而且比本文前面提到的另一种常见方法更具扩展性——例如,可以将字符串格式表达式从资源文件中加载。
string var1 = ".NET";
float var2 = 1.1;
string s = "Hello, from " + var1 + " " + var2.ToString() + "!";
我相信您会同意,字符串格式表达式在运行时更容易阅读和更改。然而,在 .NET 中格式化字符串的真正强大之处不止于此。附加的格式说明符可用于以各种方式控制数值类型的输出。
.NET 中的格式说明符
在使用 String.Format
等方法时,会使用字符串格式表达式中的格式说明符来评估带索引的占位符。这就是所谓的复合格式设置[^]。每个格式说明符都可以采用 {index[,alignment][:formatString]}
的形式。这些格式字符串几乎可以是任何内容,并且可以为特定格式定义附加选项。您还可以为格式化后的输出字符串进行填充和对齐,以适应特定数量的空格或制表符。
这些格式说明符由 StringBuilder
内部传递给 IFormattable
的实现,或者在将 IFormatProvider
作为第一个参数(对于 FCL 中的方法)传递时传递给 ICustomFormatter
。这将在稍后讨论。
Framework 类库中的格式提供程序
顾名思义,格式提供程序为类型提供格式化功能。FCL 中的两个格式提供程序是 DateTimeFormatInfo
[^] 和 NumberFormatInfo
[^] 类。两者都实现了 IFormatProvider
接口,该接口是格式提供程序在使用前面提到的字符串格式化方法时必须支持的接口。这定义了一个所有格式提供程序都必须遵守的契约,以便建立通用实现。该接口将在后面更详细地介绍。
在格式化数值类型和 DateTime
结构时,如果未提供 IFormatProvider
实现,则会自动使用上面的两个格式提供程序类来为相关类型提供格式化。以下是一个使用 DateTime
的简单示例。
DateTime now = DateTime.Now;
string.Format("Short date: {0:d}", now);
string.Format("Long date: {0:D}", now);
string.Format("Sortable date/time: {0:s}", now);
string.Format("Custom date: {0:ddd, MMM dd, yyyy}", now);
使用格式说明符,您还可以使用 ResourceManager
[^] 从嵌入式资源文件中加载字符串格式表达式。虽然当前 CultureInfo
用于格式化实际的日期、时间和数字(或者将特定区域性的 DateTimeFormatInfo
或 NumberFormatInfo
作为 IFormatProvider
参数传递),但您可以使用此方法加载本地化字符串,包括使用从右到左阅读顺序的字符串。
例如,如果您想使用其他区域性的格式化信息来格式化日期,而无需更改执行线程的 CurrentCulture
,您可以简单地获取该区域性的 DateTimeFormatInfo
。
CultureInfo culture = new CultureInfo("de-DE");
DateTime dt = DateTime.Now;
string.Format(culture.DateTimeFormat, "Großdatum: {0:D}", now);
该日期将使用德语区域性的日期和时间信息来格式化值,包括本地化的星期几和月份名称以及字符串各个元素的顺序。
如前所述,您可以进一步扩展此功能,从嵌入式资源中获取字符串格式表达式。为简单起见,假设 Thread.CurrentUICulture
已正确设置。
ResourceManager resources = new ResourceManager("Strings.resources",
GetType().Assembly);
string format = resources.GetString("CommonFormat");
DateTime dt = DateTime.Now;
string.Format(format, now);
键为“CommonFormat”的值将包含字符串格式表达式,例如“Long date: {0:D}”。这些通常存储在 ResX 文件中,这超出了本文的范围。
使用自定义格式提供程序扩展现有类型
数值类型和 DateTime
结构已有关联的格式提供程序,它们提供了许多不同的格式。DateTimeFormatInfo
甚至允许您使用自定义格式说明符。但是,如果您想格式化任何类型,并且因为格式化而无法扩展该类型或不想扩展该类型,该怎么办?
您可以实现 ICustomFormatter
接口,并通过自定义 IFormatProvider
来公开该实现,您可以将其作为第一个参数传递给 String.Format
等方法。本文的示例项目包含两个自定义格式提供程序:一个自定义 NumberFormatInfo
类,它可以将数字转换为任何基数(来自 .NET Framework SDK,包含因为它非常方便)并转换为高达 3,999,999 的罗马数字;以及一个 StringFormatInfo
类,它可以将字符串转换为摩尔斯电码(支持当前字符,包括 2004 年初刚添加的“@”)。在这两个示例中,都实现了 ICustomFormatter
和 IFormatProvider
接口,以便简化操作,而且在生产代码中您完全可以这样做。以下是 StringFormatInfo
类的声明。
public sealed class StringFormatInfo : IFormatProvider, ICustomFormatter
{
}
如您所见,该类实现了 IFormatProvider
,因此我们可以将其直接传递给 String.Format
等调用。
StringFormatInfo fmtinfo = new StringFormatInfo();
string.Format(fmtinfo, @"The morse code for ""{0}"":\n{0:m}", "Hello, world");
IFormatProvider
[^] 声明了一个名为 GetFormat(Type)
[^] 的方法。要实现此方法,请检查参数是否为 ICustomFormatter
类型,而不是您的类类型。这与格式化方法的内部实现有关。
public object GetFormat(Type formatType)
{
if (typeof(ICustomFormatter).Equals(formatType)) return this;
return null;
}
ICustomFormatter
[^] 也声明了一个名为 Format(String, Object, IFormatProvider
)[^] 的方法。这就是实际工作完成的地方。您应该首先确保要格式化的对象不为 null(Visual Basic 中为 Nothing
),除非您想用自定义格式提供程序特别处理 null 值。然后检查格式字符串——方法中声明的第一个参数。您可以选择是否执行区分大小写的字符串比较。例如,FCL 中提供的 DateTimeFormatInfo
会区分“d”和“D”以及其他几个字符。对于 StringFormatInfo
示例,执行不区分大小写的比较。
public string Format(string format, object arg, IFormatProvider formatProvider)
{
if (arg == null) throw new ArgumentNullException("arg");
if (format != null && arg is string)
{
string s = format.Trim().ToLower();
if (s.StartsWith("m"))
return FormatMorseCode(arg as string, format);
}
if (arg is IFormattable)
return ((IFormattable)arg).ToString(format, formatProvider);
else return arg.ToString();
}
在此示例中,我只是修剪格式并将其转换为小写。我也可以使用 String.Compare
,但如果您支持多个格式说明符,您不希望在每个条件中都进行不区分大小写的字符串比较的开销。之后,我检查格式说明符是否以“m”开头。如果是,我将某些参数传递给我的 FormatMorseCode
方法,您可以在示例项目中找到该方法的实现。
要记住的一个重要事项是,如果您不处理对象类型或格式说明符对您的 IFormatProvider
实现无效,您应该通过确定对象是否支持 IFormattable
接口(稍后将介绍)来适当地处理该对象;如果支持,则调用 IFormattable.ToString(String, IFormatProvider)
方法;否则,只需调用 ToString
,它由 Object
类声明,因此被每个类继承,其中一些类会重写默认实现(该实现仅打印对象的完全限定类型)。
格式提供程序还可以定义以特定方式控制格式的属性。例如,DateTimeFormatInfo
定义了许多属性来获取和设置要使用的 Calendar
、日期的本地化名称等等。示例 StringFormatInfo
提供了一个 LineWidth
属性,有助于确保点和划保持在一起以提高清晰度。如果您要分离 IFormatProvider
和 ICustomFormatter
的实现,通常应该在 IFormatProvider
实现上定义这些属性,因为它们会传递给 ICustomFormatter
实现以及稍后将讨论的 IFormattable
实现。在这种情况下,请确保您正在处理正确的类型,并通过强制转换为您的类型来获取所需的属性。在可能的情况下,将这些属性公开为格式说明符选项也可能是有益的,尽管您可能不应该使其过于复杂,而只需公开易于表示和解析的内容。
自定义格式提供程序使您能够使用一个简单的类或类集,为现有类型提供自定义格式说明符和选项。但是,当您定义自己的类型时,这会带来很多不必要的工作。
使用自定义格式扩展您的类型
在定义自己的类和结构类型时,您可以按任何想要的方式轻松提供自定义格式。但是,如果您想支持 .NET 中的标准格式化例程,则必须实现 IFormattable
。如果您只想为类型显示自定义字符串并且不需要支持多种类型,您也可以简单地覆盖从 Object 类继承的 ToString()
方法。Point
是一个例子,它只覆盖 ToString()
并以“{X=X, Y=Y}”的形式返回一个字符串。
IFormattable
[^] 再次只声明了一个方法,ToString(String, IFormatProvider)
[^]。再次出现 IFormatProvider
接口,这也是在 IFormatProvider
实现上定义格式属性的好理由。您可以检查您支持的不同类型,然后强制转换它们并获取控制输出格式所需的属性。在这个简单的例子中,我就是这样做的。
char c = divisor;
// ...
if (formatProvider is NumberFormatInfo)
if (((NumberFormatInfo)formatProvider).UseDiacritic)
c = diacritic;
实现 IFormattable.ToString
非常简单,但您也应该考虑重载 ToString
来为每种类型提供一个参数。这些参数可以简单地调用实现方法,该方法使用格式说明符和可选的 IFormatProvider
参数中的信息将您的类型格式化为字符串。
public override string ToString()
{
return ToString("g", null); // Always support "g" as default format.
}
public string ToString(string format)
{
return ToString(format, null);
}
public string ToString(IFormatProvider formatProvider)
{
return ToString(null, formatProvider);
}
public string ToString(string format, IFormatProvider formatProvider)
{
if (format == null) format = "g"; // Set default format, which is always "g".
// Continue formatting by checking format specifiers and options.
}
有关 IFormattable.ToString
如何在示例项目中实现的更多详细信息,请参见 Rational
结构示例——一个功能基本齐全的分数结构。它与前面关于为现有类型提供自定义格式提供程序的示例没有太大区别。
摘要
使用 .NET Framework 格式化字符串是一个非常灵活的系统,当您实现正确的接口时。您可以为现有类型提供自定义格式化程序,也可以在您自己的类型中实现自定义格式化。本文的示例项目演示了提供自定义格式化的两种方法,并包含一个简单的测试应用程序来尝试不同的组合。
使用自定义格式提供程序和格式化程序可以帮助简化您的代码,以便您可以从数据库或资源文件等不同源加载自定义格式表达式,并为带索引的参数提供自定义格式选项,甚至在格式表达式中重用相同的参数。只需稍加代码,您应该就能轻松地提供应用程序所需的任何字符串格式。