格式化字符串
Posted louzi
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了格式化字符串相关的知识,希望对你有一定的参考价值。
开发过程中,我们经常使用格式化字符串,本文学习下格式化字符串相关内容。
按照格式化字符串功能的进化,本文讨论下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()重载;
文中如有错误,欢迎交流指正。
参考文章
以上是关于格式化字符串的主要内容,如果未能解决你的问题,请参考以下文章