字符串是我们平时使用最多的一个类型,从 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<T>来避免装箱操作,这样它在 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 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 的时候,输出的是空行,没有任何内容。
评论