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 代码可以看得出来

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

IL in C#10/.NET6

在新版本中,会由 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)

  • 需要一个 publicAppendLiteral(string s) 方法来处理常量字符的拼接

  • 需要一个 publicAppendFormatted<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

C# 10 新特性 —— 插值字符串优化相关推荐

  1. C# 10 新特性 —— Lambda 优化

    C# 10 新特性 -- Lambda 优化 Intro C# 10 对于 Lambda 做了很多的优化,我们可以在 C# 中更加方便地使用委托和 Lambda 了,下面就来看一些示例 Lambda ...

  2. C# 10 新特性 —— 补充篇

    C# 10 新特性 -- 补充篇 Intro 前面已经写了几篇文章介绍 C# 10 新特性的文章,还有一些小的更新 Constant interpolated strings 在之前的版本中,如果想要 ...

  3. C# 10 新特性 —— 命名空间的变化

    C# 10 新特性 -- 命名空间的变化 Intro C# 10 针对命名空间做了一些改变,主要是 Global Usings 和 File-scoped Namespace,我们前面分享的示例其实也 ...

  4. JDK 5 ~ 10 新特性倾情整理

    转载自 JDK 5 ~ 10 新特性倾情整理 最近连 JDK11都在准备发布的路上了,大家都整明白了吗?也许现在大部分人还在用6-8,8的新特性都没用熟,9刚出不久,10-11就不用说了. 为了大家对 ...

  5. JavaScript高级之ECMASript 7、8 、9 、10 新特性

    第3章 ECMASript 7 新特性 3.1. Array.prototype.includes Includes 方法用来检测数组中是否包含某个元素,返回布尔类型值 3.2. 指数操作符 在ES7 ...

  6. Java 10新特性

    Java 10新特性 Java 10是其23年历史中最快的java版本.Java因其缓慢的增长和发展而受到批评,但Java 10刚刚破坏了这一概念.Java 10是一个具有许多未来变化的版本,其范围和 ...

  7. Java 10 新特性概述

    Java 10是其23年历史中最快发布的java版本.Java因其缓慢的增长和发展而受到批评,但Java 10刚刚破坏了这个概念.Java 10是一个具有许多未来变化的版本,其范围和影响可能并不明显, ...

  8. C# 10 新特性 —— CallerArgumentExpression

    C# 10 新特性 -- CallerArgumentExpression Intro C# 10 支持使用 CallerArgumentExpression 来自动地获取调用方的信息,这可以简化我们 ...

  9. Spark 开源新特性:Catalyst 优化流程裁剪

    摘要:为了解决过多依赖 Hive 的问题, SparkSQL 使用了一个新的 SQL 优化器替代 Hive 中的优化器, 这个优化器就是 Catalyst. 本文分享自华为云社区<Spark 开 ...

最新文章

  1. Linux下基于socket多线程并发通信的实现
  2. boost::core模块实现交换std的dateorder
  3. 用vmware-converter4把linux 迁移到ESX4.1中
  4. linux安装mysql5.7.18_Linux下安装mysql5.7.18版本步骤
  5. java看图_看图吧,Java
  6. python定时任务,隔月执行,隔定时执行
  7. [蛋蛋の涂鸦日记]02-致电通渠中心
  8. SQL数据库层面操作(DDL)
  9. 图片简单上色,花开花落云卷云舒。
  10. MySql的架构和历史
  11. C#照片预览,好处是图片不在项目中也可以查看
  12. 网站服务器和空间大小,网站服务器和空间大小
  13. js实现数字转化为大写金额——js技能提升
  14. Unity实现扫描透视效果
  15. java写文件描述_详解Java中的File文件类以及FileDescriptor文件描述类
  16. 我是如何面试iOS开发者的?
  17. 计算机管理文件破坏怎么办,技术丨电脑系统文件损坏,尝试这几步轻松解决
  18. python高考加分_Python将纳入浙江省新高考,你知道了吗?
  19. ppi 在线计算机,在线像素密度厘米英寸转换器(PPI)_三贝计算网_23bei.com
  20. 对抗机器学习系列——深度神经网络的盲点

热门文章

  1. LoadRunner中进程运行和线程运行区别
  2. ssh无密码公钥登陆
  3. C# Winform编程之Button
  4. plsql如何执行存储过程_如何理解Spark应用的执行过程
  5. [置顶] 动软软代码生成器使用(127.0.0.1)无法看到 SQLServer2008 新附加数据库的 原因 以及 解决方案...
  6. BZOJ3172: [Tjoi2013]单词
  7. [Android] 修改ImageView的图片颜色
  8. IOS-网络(大文件下载)
  9. quartz (一) 基于 Quartz 开发企业级任务调度应用
  10. ASP.NET MVC CheckBoxFor为什么会生成hidden input控件