C# 10 新特性 —— 插值字符串优化
Intro
字符串应该是我们平时使用的最多的一个类型,从 C# 6 开始我们开始支持了插值字符串,使得我们可以更方便的进行字符串的操作,现在很多分析器也推荐我们使用插值这种写法,这能够使得我们的代码更加清晰和简洁,C# 10 提供了更好的实现方式以及更好的性能
Interpolated string
什么是插值字符串呢?就是 $
符号开始的类似 $"Hello {name}"
这样的字符串,我们来看下面的示例
var str = $"1233";
var name = "Alice";
var hello = $"Hello {name}!";
var num = 10;
var numDesc = $"The num is {num}";
简单的插值字符串会简化,对于不需要 format 的参数会直接简化为字符串,对于一些简单的字符串拼接,可以简化成 string.Concat
,在 C#10/.NET 6 之前的版本中,其他的大多会翻译成 string.Format
的形式,翻译成低版本的 C# 代码则是这样的
string str = "1233";
string name = "Alice";
string hello = string.Concat("Hello ", name, "!");
int num = 10;
string numDesc = string.Format("The num is {0}", num);
对于 string.Format
,参数如果是值类型会发生装箱,变为 object
,我们从 IL 代码可以看得出来
插值字符串格式化的时候会使用当前
CultureInfo
,如果需要使用不同CultureInfo
或者手动指定,可以借助FormattableString
/FormattableStringFactory
来实现var num = 10; FormattableString str1 = $"Hello {num}"; Console.WriteLine(str1.Format); Console.WriteLine(str1.ToString(new CultureInfo("zh-CN")));str1 = FormattableStringFactory.Create("Hello {0}", num); Console.WriteLine(str1.Format); Console.WriteLine(str1.ToString(new CultureInfo("en-US")));
对于 C# 10/.NET6 中,则会生成下面的代码:
string str = "1233";
string name = "Alice";
string hello = string.Concat ("Hello ", name, "!");
int num = 10;
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(11, 1);
defaultInterpolatedStringHandler.AppendLiteral("The num is ");
defaultInterpolatedStringHandler.AppendFormatted(num);
string numDesc = defaultInterpolatedStringHandler.ToStringAndClear();
在新版本中,会由 DefaultInterpolatedStringHandler
来处理插值字符串,而且这个新的 DefaultInterpolatedStringHandler
是一个结构体并且会有一个泛型方法 AppendFormatted<T>
来避免发生装箱,在 format 的时候性能更优,对于普通的字符串则使用 AppendLiteral()
方法处理,声明如下:
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();}
}
具体实现可以参考:https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler.cs
在 .NET 6 中增加了两个 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();
Custom Interpolated string handler
接着我们来尝试实现一个简单的插值字符串处理器,实现一个最基本的插值字符串处理器需要满足四个条件:
构造函数至少需要两个
int
参数,一个是字符串中常量字符的长度(literalLength
),一个是需要格式化的参数的数量(formattedCount
)需要一个
public
的AppendLiteral(string s)
方法来处理常量字符的拼接需要一个
public
的AppendFormatted<T>(T t)
方法来处理参数自定义的处理器需要使用
InterpolatedStringHandler
来标记,处理器可以是class
也可以是struct
// InterpolatedStringHandlerAttribute is required for custom InterpolatedStringHandler
[InterpolatedStringHandler]
public struct CustomInterpolatedStringHandler
{// Storage for the built-up stringprivate readonly StringBuilder builder;/// <summary>/// CustomInterpolatedStringHandler constructor/// </summary>/// <param name="literalLength">string literal length</param>/// <param name="formattedCount">formatted count</param>public CustomInterpolatedStringHandler(int literalLength, int formattedCount){builder = new StringBuilder(literalLength);Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");}// Requiredpublic void AppendLiteral(string s){Console.WriteLine($"\tAppendLiteral called: {{{s}}}");builder.Append(s);Console.WriteLine($"\tAppended the literal string");}// Requiredpublic void AppendFormatted<T>(T t){Console.WriteLine($"\tAppendFormatted called: {{{t}}} is of type {typeof(T)}");builder.Append(t?.ToString());Console.WriteLine($"\tAppended the formatted object");}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());
}// Custom InterpolatedStringHandler
LogInterpolatedString("The num is 10");
LogInterpolatedString($"The num is {num}");
输出结果如下:
LogInterpolatedString
The num is 10literal length: 11, formattedCount: 1AppendLiteral called: {The num is }Appended the literal stringAppendFormatted called: {10} is of type System.Int32Appended the formatted object
LogInterpolatedString
CustomInterpolatedStringHandler
The num is 10
除此之外,我们还可以在自定义的插值字符串处理器的构造器中增加自定义参数,我们可以使用 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);Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");_limit = limit;}// Requiredpublic void AppendLiteral(string s){Console.WriteLine($"\tAppendLiteral called: {{{s}}}");builder.Append(s);Console.WriteLine($"\tAppended the literal string");}// Requiredpublic void AppendFormatted<T>(T t){Console.WriteLine($"\tAppendFormatted called: {{{t}}} is of type {typeof(T)}");if (t is int n && n < _limit){return;}builder.Append(t?.ToString());Console.WriteLine($"\tAppended the formatted object");}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, $"The num is {num}");
Console.WriteLine();
LogInterpolatedString(15, $"The num is {num}");
输出结果如下:
literal length: 11, formattedCount: 1AppendLiteral called: {The num is }Appended the literal stringAppendFormatted called: {10} is of type System.Int32Appended the formatted object
LogInterpolatedString
CustomInterpolatedStringHandler with limit:10
The num is 10literal length: 11, formattedCount: 1AppendLiteral called: {The num is }Appended the literal stringAppendFormatted called: {10} is of type System.Int32
LogInterpolatedString
CustomInterpolatedStringHandler with limit:15
The num is
从上面的结果可以看出来,我们的代码是生效的,第一次打印出来了 num
,第二次没有打印 num
还有一个特殊的参数,我们可以在构造方法中引入一个 bool
类型的 out
参数,如果这个参数为 false
则不会进行字符串的拼接 Append
,我们改造一下刚才的示例,示例代码如下:
public CustomInterpolatedStringHandler(int literalLength, int formattedCount, int limit, out bool shouldAppend)
{shouldAppend = limit < 20;builder = new StringBuilder(shouldAppend ? literalLength : 0);Console.WriteLine($"\tliteral length: {literalLength}, formattedCount: {formattedCount}");_limit = limit;
}
当 limit
参数小于 20 时进行字符串的拼接,否则就不输出,测试代码如下
LogInterpolatedString(10, $"The num is {num}");
Console.WriteLine();
LogInterpolatedString(15, $"The num is {num}");
Console.WriteLine();
LogInterpolatedString(20, $"The num is {num}");
输出结果是这样的
literal length: 11, formattedCount: 1AppendLiteral called: {The num is }Appended the literal stringAppendFormatted called: {10} is of type System.Int32Appended the formatted object
LogInterpolatedString
CustomInterpolatedStringHandler with limit:10
The num is 10literal length: 11, formattedCount: 1AppendLiteral called: {The num is }Appended the literal stringAppendFormatted called: {10} is of type System.Int32
LogInterpolatedString
CustomInterpolatedStringHandler with limit:15
The num isliteral length: 11, formattedCount: 1
LogInterpolatedString
CustomInterpolatedStringHandler with limit:20
可以看到,当 limit 是 20 的时候,输出的是空行,没有任何内容
另外我们可以把上面的 Append
方法的返回值改成 bool
,如果方法中返回 false
则会造成短路,类似于 ASP.NET Core 中中间件的短路,后面的拼接就会取消,我们再改造一下上面的示例,改造一下 Append
方法
public bool AppendLiteral(string s)
{if (s.Length <= 1)return false;Console.WriteLine($"\tAppendLiteral called: {{{s}}}");builder.Append(s);Console.WriteLine($"\tAppended the literal string");return true;
}// Required
public bool AppendFormatted<T>(T t)
{Console.WriteLine($"\tAppendFormatted called: {{{t}}} is of type {typeof(T)}");if (t is int n && n < _limit){return false;}builder.Append(t?.ToString());Console.WriteLine($"\tAppended the formatted object");return true;
}
再来使用 LogInterpolatedString(12, $"The num is {num} and the time is {DateTime.Now}!");
调用一下试一下,输出结果如下:
literal length: 29, formattedCount: 2AppendLiteral called: {The num is }Appended the literal stringAppendFormatted called: {10} is of type System.Int32
LogInterpolatedString
CustomInterpolatedStringHandler with limit:12
The num is
更多自定义可以参考默认的 DefaultInterpolatedStringHandler
使用自定义的
InterpolatedStringHandler
时,如果是结构体,参数建议使用ref
引用传递,可以参考 https://github.com/dotnet/runtime/issues/57538
More
有哪些场景可以用呢?下面就是一个示例,更多细节可以参考:https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Debug.cs
https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Debug.cs#L280
[Conditional("DEBUG")]
public static void Assert([DoesNotReturnIf(false)] bool condition, [InterpolatedStringHandlerArgument("condition")] ref AssertInterpolatedStringHandler message) =>Assert(condition, message.ToStringAndClear());
当然不仅于此,还有很多细节可以去挖掘,还有 StringBuilder
/Memory
等也使用了新的方式来处理插值字符串
最后如果我们可以使用插值字符串,就尽可能地使用插值字符串来处理,从 .NET 6 以后就不会有装箱的问题了,性能还会更好
感兴趣的小伙伴们可以更加深入研究一下,上面的示例有需要的可以从 Github 上获取 https://github.com/WeihanLi/SamplesInPractice/blob/master/CSharp10Sample/InterpolatedStringSample.cs
References
https://github.com/dotnet/runtime/issues/50635
https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/InterpolatedStringHandlerAttribute.cs
https://github.com/dotnet/csharplang/blob/main/proposals/csharp-10.0/improved-interpolated-strings.md
https://devblogs.microsoft.com/dotnet/string-interpolation-in-c-10-and-net-6/
https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/DefaultInterpolatedStringHandler.cs
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/interpolated#compilation-of-interpolated-strings
https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.defaultinterpolatedstringhandler?view=net-6.0
https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/interpolated-string-handler
https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-10.0/improved-interpolated-strings
https://docs.microsoft.com/en-us/dotnet/csharp/tutorials/string-interpolation
https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Debug.cs
https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/FormattableString.cs
https://github.com/dotnet/runtime/blob/v6.0.0/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/FormattableStringFactory.cs
https://github.com/dotnet/runtime/issues/57538
https://github.com/WeihanLi/SamplesInPractice/blob/master/CSharp10Sample/InterpolatedStringSample.cs