格式化字符串

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.Shared申请内存。发出一系列的调用追加插入的字符串,调用AppendLiteral()追加字符串常量部分,调用AppendFormatted()合适的重载追加格式化的部分。调用ToStringAndClear()方法提取构建好的字符串并将资源返回给ArrayPool.Shared。

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()重载;

文中如有错误,欢迎交流指正。

参考文章

转载请注明出处,欢迎交流。

以上是关于格式化字符串的主要内容,如果未能解决你的问题,请参考以下文章

angularjs怎么将字符串格式化

格式化字符串是啥意思?

PWN格式化字符串2——例子

格式化字符串f

C#格式化字符串

如何使用JS在HTML中自定义字符串格式化