Java教程

格式化字符串

本文主要是介绍格式化字符串,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

开发过程中,我们经常使用格式化字符串,本文学习下格式化字符串相关内容。

按照格式化字符串功能的进化,本文讨论下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()重载;

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

参考文章

  • string interpolation
  • Back to Basics: String Interpolation in C#
  • String Interpolation in C# 10 and .NET 6
  • Welcome to C# 10
  • Improved Interpolated Strings
  • String.cs
  • DefaultInterpolatedStringHandler.cs
  • 彻底学通string.Format以及IFormattable,IFormatProvider,ICustomFormatter
  • C# 10 新特性 —— 插值字符串优化
这篇关于格式化字符串的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!