格式化字符串
开发过程中,我们经常使用格式化字符串,本文学习下格式化字符串相关内容。
按照格式化字符串功能的进化,本文讨论下String.Format(),C# 6版本的字符串内插及C#10版本的字符串内插优化。
String.Format()
实现格式化字符串有多种方法,如可以使用简单的字符串相加,但是这种方式可读性较差。
最常用的是String.Format()方法,该方法是String的一个静态方法,有多种形式的重载,其内部使用StringBuilder的Append()方法进行拼接。
如果字符串中需要包含'{'或'}',需使用'{{'或'}}'进行转义。当遇到'{'字符时,如果不是两个'{',则会获取{}内索引对应的参数,并调用其ToString()方法,然后使用Append()拼接到StringBuilder。
如果需要对齐参数,可以在{}内使用','指定对齐方式。
如果需要格式化参数,可以在{}内使用':'指定格式化方式,Format()方法会检查参数是否实现了IFormattable,是则调用IFormattable.ToString(String format, IFormatProvider formatProvider)方法获取格式化后的字符串进行拼接。因此,如果想自定义类型格式化形式,需实现IFormattable接口。当然也可以实现IFormatProvider和ICustomFormatter接口,并将IFormatProvider的实现类作为参数传入。
public static String Format(String format, Object arg0);
public static String Format(String format, Object arg0, Object arg1);
public static String Format(String format, Object arg0, Object arg1, Object arg2);
public static String Format(String format, params Object[] args);
public static String Format(IFormatProvider provider, String format, Object arg0);
public static String Format(IFormatProvider provider, String format, Object arg0, Object arg1);
public static String Format(IFormatProvider provider, String format, Object arg0, Object arg1, Object arg2);
public static String Format(IFormatProvider provider, String format, params Object[] args);
private static String FormatHelper(IFormatProvider provider, String format, ParamsArray args) {
if (format == null)
throw new ArgumentNullException("format");
return StringBuilderCache.GetStringAndRelease(StringBuilderCache.Acquire(format.Length + args.Length * 8).AppendFormatHelper(provider, format, args));
}
internal StringBuilder AppendFormatHelper(IFormatProvider provider, String format, ParamsArray args) {
if (format == null) {
throw new ArgumentNullException("format");
}
Contract.Ensures(Contract.Result<StringBuilder>() != null);
Contract.EndContractBlock();
int pos = 0;
int len = format.Length;
char ch = '\x0';
ICustomFormatter cf = null;
if (provider != null) {
cf = (ICustomFormatter)provider.GetFormat(typeof(ICustomFormatter));
}
while (true) {
int p = pos;
int i = pos;
while (pos < len) {
ch = format[pos];
pos++;
if (ch == '}')
{
if (pos < len && format[pos] == '}') // Treat as escape character for }}
pos++;
else
FormatError();
}
if (ch == '{')
{
if (pos < len && format[pos] == '{') // Treat as escape character for {{
pos++;
else
{
pos--;
break;
}
}
Append(ch);
}
if (pos == len) break;
pos++;
if (pos == len || (ch = format[pos]) < '0' || ch > '9') FormatError();
int index = 0;
do {
index = index * 10 + ch - '0';
pos++;
if (pos == len) FormatError();
ch = format[pos];
} while (ch >= '0' && ch <= '9' && index < 1000000);
if (index >= args.Length) throw new FormatException(Environment.GetResourceString("Format_IndexOutOfRange"));
while (pos < len && (ch = format[pos]) == ' ') pos++;
bool leftJustify = false;
int width = 0;
if (ch == ',') {
pos++;
while (pos < len && format[pos] == ' ') pos++;
if (pos == len) FormatError();
ch = format[pos];
if (ch == '-') {
leftJustify = true;
pos++;
if (pos == len) FormatError();
ch = format[pos];
}
if (ch < '0' || ch > '9') FormatError();
do {
width = width * 10 + ch - '0';
pos++;
if (pos == len) FormatError();
ch = format[pos];
} while (ch >= '0' && ch <= '9' && width < 1000000);
}
while (pos < len && (ch = format[pos]) == ' ') pos++;
Object arg = args[index];
StringBuilder fmt = null;
if (ch == ':') {
pos++;
p = pos;
i = pos;
while (true) {
if (pos == len) FormatError();
ch = format[pos];
pos++;
if (ch == '{')
{
if (pos < len && format[pos] == '{') // Treat as escape character for {{
pos++;
else
FormatError();
}
else if (ch == '}')
{
if (pos < len && format[pos] == '}') // Treat as escape character for }}
pos++;
else
{
pos--;
break;
}
}
if (fmt == null) {
fmt = new StringBuilder();
}
fmt.Append(ch);
}
}
if (ch != '}') FormatError();
pos++;
String sFmt = null;
String s = null;
if (cf != null) {
if (fmt != null) {
sFmt = fmt.ToString();
}
s = cf.Format(sFmt, arg, provider);
}
if (s == null) {
IFormattable formattableArg = arg as IFormattable;
#if FEATURE_LEGACYNETCF
if(CompatibilitySwitches.IsAppEarlierThanWindowsPhone8) {
// TimeSpan does not implement IFormattable in Mango
if(arg is TimeSpan) {
formattableArg = null;
}
}
#endif
if (formattableArg != null) {
if (sFmt == null && fmt != null) {
sFmt = fmt.ToString();
}
s = formattableArg.ToString(sFmt, provider);
} else if (arg != null) {
s = arg.ToString();
}
}
if (s == null) s = String.Empty;
int pad = width - s.Length;
if (!leftJustify && pad > 0) Append(' ', pad);
Append(s);
if (leftJustify && pad > 0) Append(' ', pad);
}
return this;
}
字符串内插(C# 6)
C# 6推出了字符串内插语法,对比String.Format()方法:
- 代码可读性更高:尤其是结合@多行显示长字符串时,代码更易读;
- 降低了犯错的风险:使用String.Format()需注意占位符索引、参数顺序及参数个数,字符串内插无需注意;
- 实现方式一致:字符串内插在编译时会被编译成对String.Format()方法的调用(如果行为等同于串联则生成对String.Concat()的调用);
- 性能有微乎其微的影响:显示变量内插会导致一点开销但开销很小。
// source code
string name = "world";
Console.WriteLine($"hello {name}");
int i = 10;
Console.WriteLine($"i: {i}");
// IL code
0000 nop
0001 ldstr "world"
0006 stloc.0
0007 ldstr "hello "
000C ldloc.0
000D call string [mscorlib]System.String::Concat(string, string)
0012 call void [mscorlib]System.Console::WriteLine(string)
0017 nop
0018 ldc.i4.s 10
001A stloc.1
001B ldstr "i: {0}"
0020 ldloc.1
0021 box [mscorlib]System.Int32
0026 call string [mscorlib]System.String::Format(string, object)
002B call void [mscorlib]System.Console::WriteLine(string)
0030 nop
0031 ret
字符串内插优化(C# 10)
从上文中的IL代码可以看到,调用C# 6版本的字符串内插的时候,出现了装箱操作,因此是有性能问题的。总结C# 6字符串内插的一些性能、开销、使用问题如下:
- 值类型参数会被装箱;
- 大多数情况下会分配一个参数数组;
- 无法使用Span或其它的ref struct类型;
- 无法给常量字符串赋值;
- 当条件不成立无需创建字符串的情况下,String.Format()无法避免执行,如
Debug.Assert(condition, $"{SomethingExpensiveHappensHere()}");
; - 当进行插值时,不仅需调用参数的Object.ToString()或IFormattable.ToString,还要分配临时的string对象;
C# 10对字符串内插进行了优化,如下.NET 6代码编译后使用DnSpy查看反编译后的C#代码,可以看到其实现不再是调用String.Format(),而是由DefaultInterpolatedStringHandler处理字符串内插。
// source code
int i = 10;
Console.WriteLine($"i: {i}");
// 反编译后
int i = 10;
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(3, 1);
defaultInterpolatedStringHandler.AppendLiteral("i: ");
defaultInterpolatedStringHandler.AppendFormatted<int>(i);
Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear());
DefaultInterpolatedStringHandler声明如下,详细实现可参考源码。编译器根据传入的literalLength和formattedCount参数估计并从ArrayPool
namespace System.Runtime.CompilerServices
{
[InterpolatedStringHandler]
public ref struct DefaultInterpolatedStringHandler
{
public DefaultInterpolatedStringHandler(int literalLength, int formattedCount);
public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, System.IFormatProvider? provider);
public DefaultInterpolatedStringHandler(int literalLength, int formattedCount, System.IFormatProvider? provider, System.Span<char> initialBuffer);
public void AppendLiteral(string value);
public void AppendFormatted<T>(T value);
public void AppendFormatted<T>(T value, string? format);
public void AppendFormatted<T>(T value, int alignment);
public void AppendFormatted<T>(T value, int alignment, string? format);
public void AppendFormatted(ReadOnlySpan<char> value);
public void AppendFormatted(ReadOnlySpan<char> value, int alignment = 0, string? format = null);
public void AppendFormatted(string? value);
public void AppendFormatted(string? value, int alignment = 0, string? format = null);
public void AppendFormatted(object? value, int alignment = 0, string? format = null);
public string ToStringAndClear();
}
}
总结C# 10对字符串内插进行优化后,有如下改进:
- 对于内插参数使用泛型方法AppendFormatted
避免了格式化参数装箱操作; - 每个插值都会有对应的AppendFormatted()重载调用,因此当传递多个参数时无需分配参数数组;
- 通过AppendFormatted(ReadOnlySpan
)方法,可以使用Span作为格式化参数; - 无需在运行时解析插值字符串,编译时进行了解析并生成了一系列的调用以便运行时构建字符串;
- 提供ISpanFormattable接口,取代对object.ToString()或IFormattable.ToString()的调用,无需生成临时string。core libraries中的很多类型已实现该接口,提供更好的性能生成字符串;
- String提供了两个静态的Create()方法重载,通过传入IFormatProvider及Span
进一步优化性能; - StringBuilder类优化:提供Append()及AppendLine()的重载,支持字符串内插形式以优化性能;
- 当条件不成立时,可根据out bool参数,跳过AppendLiteral()及AppendFormatted(),如.NET 6中的Debug.Assert()重载;
文中如有错误,欢迎交流指正。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)