字符串是我们平时使用最多的一个类型,从C#6开始就支持插值字符串,方便我们进行字符串的操作,并且大部分分析器也推荐使用插值这种写法,因为它够使得我们的代码更加清晰简洁,到了.NET6中的C#10则为我们提供了更好的实现方式以及更佳的性能。
那么什么是插值字符串呢?它是以$符开头的,类似于 $“Hello {name}” 这样的字符串,下面的例子是插值字符串的简单使用:
var name = "插值字符串"; var hello = $"你好 {name}!"; var num= 10; var numMessage= $"我喜欢数字 {num}";
我们不需要使用format就可以直接简化字符串拼接,并且对于一些简单的字符串拼接可以简化成string.Concat
,在.NET6之前的版本中它会被翻译成低版本C#中的string.Format形式,上述代码翻译成低版本C# 代码如下所示:
string name = "插值字符串"; string hello = string.Concat("你好 ", name, "!"); int num= 10; string numMessage= string.Format("我喜欢数字 {0}", );
对于string.Format来说,如果参数是值类型会发生装箱,变为 object,这一点我们可以通过IL代码看出。这里需要注意的是插值字符串格式化的时候会使用当前的CultureInfo,如果我们需要使用不同的CultureInfo或手动指定CultureInfo,那么可以使用FormattableString或FormattableStringFactory来实现。代码如下会根据指定的CultureInfo显示出不同的数字格式:
var id=35000; FormattableString str1 = $"id是{id}"; Console.WriteLine(str1.Format); Console.WriteLine(str1.ToString(new CultureInfo("zh-CN"))); str1 = FormattableStringFactory.Create("Hello {0}", id); Console.WriteLine(str1.Format); Console.WriteLine(str1.ToString(new CultureInfo("en-US")));
在.NET6 中本文的第一段代码会翻译成生成下面这样的:
string name = "插值字符串"; string hello = string.Concat ("Hello ", name, "!"); int num= 10; DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(11, 1); defaultInterpolatedStringHandler.AppendLiteral("我喜欢数字 "); defaultInterpolatedStringHandler.AppendFormatted(num); string numDesc = defaultInterpolatedStringHandler.ToStringAndClear();
在.NET6中会由DefaultInterpolatedStringHandler处理插值字符串。它DefaultInterpolatedStringHandler是结构体,并且包含泛型方法AppendFormatted来避免装箱操作,这样它在format的时候性能更好。并且在.NET6中String增加了两个方法来支持使用新的插值处理方式,新增的方法代码如下所示:
/// <summary>Creates a new string by using the specified provider to control the formatting of the specified interpolated string.</summary> /// <param name="provider">An object that supplies culture-specific formatting information.</param> /// <param name="handler">The interpolated string.</param> /// <returns>The string that results for formatting the interpolated string using the specified format provider.</returns> public static string Create(IFormatProvider? provider, [InterpolatedStringHandlerArgument("provider")] ref DefaultInterpolatedStringHandler handler) => handler.ToStringAndClear(); /// <summary>Creates a new string by using the specified provider to control the formatting of the specified interpolated string.</summary> /// <param name="provider">An object that supplies culture-specific formatting information.</param> /// <param name="initialBuffer">The initial buffer that may be used as temporary space as part of the formatting operation. The contents of this buffer may be overwritten.</param> /// <param name="handler">The interpolated string.</param> /// <returns>The string that results for formatting the interpolated string using the specified format provider.</returns> public static string Create(IFormatProvider? provider, Span<char> initialBuffer, [InterpolatedStringHandlerArgument("provider", "initialBuffer")] ref DefaultInterpolatedStringHandler handler) => handler.ToStringAndClear();
下面我们来实现一个简单的插值字符串处理器,实现一个最基本的插值字符串处理器需要满足以下四个条件:
- 构造函数至少需要两个int参数,一个是字符串中常量字符的长度,一个是需要格式化的参数的数量;
- 需要具有public的AppendLiteral(string s)方法处理常量字符的拼接;
- 需要具有public的AppendFormatted(T t)方法处理参数;
- 自定义处理器需要使用InterpolatedStringHandler标记,并且处理器可以是class也可以是struct。下面的代码就实现了一个简单的插值字符串处理器
[InterpolatedStringHandler] public struct CustomInterpolatedStringHandler { private readonly StringBuilder builder; public CustomInterpolatedStringHandler(int literalLength, int formattedCount) { builder = new StringBuilder(literalLength); } public void AppendLiteral(string s) { builder.Append(s); } public void AppendFormatted<T>(T t) { builder.Append(t?.ToString()); } public override string ToString() { return builder.ToString(); } }
当我们使用它的时候,可以这么用:
private static void LogInterpolatedString(string str) { Console.WriteLine(nameof(LogInterpolatedString)); Console.WriteLine(str); } private static void LogInterpolatedString(CustomInterpolatedStringHandler stringHandler) { Console.WriteLine(nameof(LogInterpolatedString)); Console.WriteLine(nameof(CustomInterpolatedStringHandler)); Console.WriteLine(stringHandler.ToString()); } LogInterpolatedString("我喜欢的数字是10"); int num=20; LogInterpolatedString($"我喜欢的数字是{num}");
输出结果如下:
LogInterpolatedString 我喜欢的数字是10 LogInterpolatedString CustomInterpolatedStringHandler 我喜欢的数字是20
我们还可以在自定义的插值字符串处理器的构造器中增加自定义参数,使用InterpolatedStringHandlerArgument来引入更多构造器参数。我们改造一下上面CustomInterpolatedStringHandler代码:
[InterpolatedStringHandler] public struct CustomInterpolatedStringHandler { private readonly StringBuilder builder; private readonly int _limit; public CustomInterpolatedStringHandler(int literalLength, int formattedCount) : this(literalLength, formattedCount, 0) { } public CustomInterpolatedStringHandler(int literalLength, int formattedCount, int limit) { builder = new StringBuilder(literalLength); _limit = limit; } public void AppendLiteral(string s) { builder.Append(s); } public void AppendFormatted<T>(T t) { if (t is int n && n < _limit) { return; } builder.Append(t?.ToString()); } public override string ToString() { return builder.ToString(); } }
我们修改调用代码:
private static void LogInterpolatedString(int limit, [InterpolatedStringHandlerArgument("limit")] ref CustomInterpolatedStringHandler stringHandler) { Console.WriteLine(nameof(LogInterpolatedString)); Console.WriteLine($"{nameof(CustomInterpolatedStringHandler)} with limit:{limit}"); Console.WriteLine(stringHandler.ToString()); }
在调用代码中我们做了一个检查,如果参数是int并且小于传入的limit参数则不会被拼接,下面我们再来修改调用代码:
LogInterpolatedString(10, $"我喜欢的数字是{num}"); Console.WriteLine(); LogInterpolatedString(15, $"我喜欢的数字是{num}");
输出结果如下:
LogInterpolatedString CustomInterpolatedStringHandler with limit:10 我喜欢的数字是10 LogInterpolatedString CustomInterpolatedStringHandler with limit:15 我喜欢的数字是
从上面输出的结果可以看出第一次打印出来了 num,第二次没有打印 num。
其实还有一个特殊的参数,我们可以在构造方法中引入一个bool类型的out参数,如果值为false则不会进行字符串的拼接,我们再次改造一下前面的代码:
public CustomInterpolatedStringHandler(int literalLength, int formattedCount, int limit, out bool shouldAppend) { shouldAppend = limit < 30; builder = new StringBuilder(shouldAppend ? literalLength : 0); _limit = limit; }
当limit参数小于30时进行字符串的拼接,否则就不输出,调用代码修改如下:
LogInterpolatedString(10, $"我喜欢的数字是 {num}"); Console.WriteLine(); LogInterpolatedString(15, $"我喜欢的数字是{num}"); Console.WriteLine(); LogInterpolatedString(30, $"我喜欢的数字是{num}");
输出结果是这样的
LogInterpolatedString CustomInterpolatedStringHandler with limit:10 我喜欢的数字是10 LogInterpolatedString CustomInterpolatedStringHandler with limit:15 我喜欢的数字是 LogInterpolatedString CustomInterpolatedStringHandler with limit:30
可以看到,当limit是30 的时候,输出的是空行,没有任何内容。