字符串插值

字符串插值允许开发人员将 variables 和 text 组合在一起形成一个字符串。

基本例子

创建了两个 int 变量:foobar

int foo = 34;
int bar = 42;

string resultString = $"The foo is {foo}, and the bar is {bar}.";

Console.WriteLine(resultString);

输出

foo 是 34,bar 是 42。

查看演示

仍然可以使用字符串中的大括号,如下所示:

var foo = 34;
var bar = 42;

// String interpolation notation (new style)
Console.WriteLine($"The foo is {{foo}}, and the bar is {{bar}}.");

这会产生以下输出:

foo 是{foo},条形是{bar}。

使用插值与逐字字符串文字

在字符串之前使用 @ 会导致字符串被逐字解释。因此,例如 Unicode 字符或换行符将保持与键入的完全一致。但是,这不会影响插值字符串中的表达式,如以下示例所示:

Console.WriteLine($@"In case it wasn't clear:
\u00B9
The foo
is {foo},
and the bar
is {bar}.");

输出:

如果不清楚:
\ u00B9
foo
是 34,
条形
是 42。

查看演示

表达式

使用字符串插值,也可以计算花括号 {} 中的表达式。结果将插入字符串中的相应位置。例如,要计算 foobar 的最大值并插入它,请在花括号内使用 Math.Max

Console.WriteLine($"And the greater one is: { Math.Max(foo, bar) }");

输出:

更大的是:42

注意:大括号和表达式之间的任何前导或尾随空格(包括空格,制表符和 CRLF /换行符)都将被完全忽略,并且不包含在输出中

查看演示

另一个例子,变量可以格式化为货币:

Console.WriteLine($"Foo formatted as a currency to 4 decimal places: {foo:c4}");

输出:

Foo 格式化为 4 位小数的货币:$ 34.0000

查看演示

或者它们可以格式化为日期:

Console.WriteLine($"Today is: {DateTime.Today:dddd, MMMM dd - yyyy}");

输出:

今天是:2015 年 7 月 20 日星期一

查看演示

也可以在插值中评估带条件(三元)运算符的语句。但是,这些必须用括号括起来,因为冒号用于表示格式,如上所示:

Console.WriteLine($"{(foo > bar ? "Foo is larger than bar!" : "Bar is larger than foo!")}");

输出:

bar 比 foo 大!

查看演示

条件表达式和格式说明符可以混合使用:

Console.WriteLine($"Environment: {(Environment.Is64BitProcess ? 64 : 32):00'-bit'} process");

输出:

环境:32 位进程

转义序列

对于逐字和非逐字字符串文字,转义反斜杠(\)和引用(")字符在插值字符串中的工作方式与非插值字符串完全相同:

Console.WriteLine($"Foo is: {foo}. In a non-verbatim string, we need to escape \" and \\ with backslashes.");
Console.WriteLine($@"Foo is: {foo}. In a verbatim string, we need to escape "" with an extra quote, but we don't need to escape \");

输出:

Foo 是 34.在一个非逐字的字符串中,我们需要转义“和\用反斜杠
.Foo 是 34.在一个逐字字符串中,我们需要通过额外的引用来转义,但我们不需要转义\

要在插值字符串中包含大括号 {},请使用两个花括号 {{}}

$"{{foo}} is: {foo}"

输出:

{foo}是:34

查看演示

FormattableString 类型

$"..." 字符串插值表达式的类型并不总是简单的字符串。编译器根据上下文决定分配哪种类型:

string s = $"hello, {name}";
System.FormattableString s = $"Hello, {name}";
System.IFormattable s = $"Hello, {name}";

当编译器需要选择要调用哪个重载方法时,这也是类型首选项的顺序。

一个新型System.FormattableString,表示一个复合格式字符串,随着要被格式化的参数。使用它来编写专门处理插值参数的应用程序:

public void AddLogItem(FormattableString formattableString)
{
    foreach (var arg in formattableString.GetArguments())
    {
        // do something to interpolation argument 'arg'
    }

    // use the standard interpolation and the current culture info
    // to get an ordinary String:
    var formatted = formattableString.ToString();

    // ...
}

调用上面的方法:

AddLogItem($"The foo is {foo}, and the bar is {bar}.");

例如,如果日志记录级别已经过滤掉日志项,则可以选择不产生格式化字符串的性能成本。

隐含的转换

内插字符串有隐式类型转换:

var s = $"Foo: {foo}";
System.IFormattable s = $"Foo: {foo}";

你还可以生成一个 IFormattable 变量,允许你使用不变上下文转换字符串:

var s = $"Bar: {bar}";
System.FormattableString s = $"Bar: {bar}";

现在和不变的文化方法

如果打开代码分析,插值字符串将全部产生警告 CA1305 (指定 IFormatProvider)。静态方法可用于应用当前文化。

public static class Culture
{
    public static string Current(FormattableString formattableString)
    {
        return formattableString?.ToString(CultureInfo.CurrentCulture);
    }
    public static string Invariant(FormattableString formattableString)
    {
        return formattableString?.ToString(CultureInfo.InvariantCulture);
    }
}

然后,要为当前文化生成正确的字符串,只需使用以下表达式:

Culture.Current($"interpolated {typeof(string).Name} string.")
Culture.Invariant($"interpolated {typeof(string).Name} string.")

注意CurrentInvariant 不能创建为扩展方法,因为默认情况下,编译器将类型 String 分配给插值字符串表达式,导致以下代码无法编译:

$"interpolated {typeof(string).Name} string.".Current();

FormattableString 类已经包含了 Invariant() 方法,所以转换到不变文化的最简单方法是依靠 using static

using static System.FormattableString;
string invariant = Invariant($"Now = {DateTime.Now}");
string current = $"Now = {DateTime.Now}";

在幕后

插值字符串只是 String.Format() 的语法糖。编译器( Roslyn )将在幕后将其变为 String.Format

var text = $"Hello {name + lastName}";

以上内容将转换为以下内容:

string text = string.Format("Hello {0}", new object[] {
    name + lastName
});

字符串插值和 Linq

可以在 Linq 语句中使用插值字符串来进一步提高可读性。

var fooBar = (from DataRow x in fooBarTable.Rows
          select string.Format("{0}{1}", x["foo"], x["bar"])).ToList();

可以重写为:

var fooBar = (from DataRow x in fooBarTable.Rows
          select $"{x["foo"]}{x["bar"]}").ToList();

可重复使用的插值字符串

使用 string.Format,你可以创建可重用的格式字符串:

public const string ErrorFormat = "Exception caught:\r\n{0}";

// ...

Logger.Log(string.Format(ErrorFormat, ex));

但是,插值字符串不会使用占位符引用不存在的变量进行编译。以下内容无法编译:

public const string ErrorFormat = $"Exception caught:\r\n{error}";
// CS0103: The name 'error' does not exist in the current context

相反,创建一个消耗变量并返回 StringFunc<>

public static Func<Exception, string> FormatError =
    error => $"Exception caught:\r\n{error}";

// ...

Logger.Log(FormatError(ex));

字符串插值和本地化

如果你正在本地化你的应用程序,你可能想知道是否可以使用字符串插值和本地化。确实,有可能存储资源文件 Strings,如:

"My name is {name} {middlename} {surname}"

而不是可读性低得多:

"My name is {0} {1} {2}"

String 插值过程在编译时发生,与在运行时发生的带有 string.Format 的格式化字符串不同。插值字符串中的表达式必须引用当前上下文中的名称,并且需要存储在资源文件中。这意味着如果你想使用本地化,你必须这样做:

var FirstName = "John";

// method using different resource file "strings"
// for French ("strings.fr.resx"), German ("strings.de.resx"), 
// and English ("strings.en.resx")
void ShowMyNameLocalized(string name, string middlename = "", string surname = "")
{
    // get localized string
    var localizedMyNameIs = Properties.strings.Hello;
    // insert spaces where necessary
    name = (string.IsNullOrWhiteSpace(name) ? "" : name + " ");
    middlename = (string.IsNullOrWhiteSpace(middlename) ? "" : middlename + " ");
    surname = (string.IsNullOrWhiteSpace(surname) ? "" : surname + " ");
    // display it
    Console.WriteLine($"{localizedMyNameIs} {name}{middlename}{surname}".Trim());
}

// switch to French and greet John
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("fr-FR");
ShowMyNameLocalized(FirstName);

// switch to German and greet John
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("de-DE");
ShowMyNameLocalized(FirstName);

// switch to US English and greet John
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");
ShowMyNameLocalized(FirstName);

如果上面使用的语言的资源字符串正确存储在各个资源文件中,则应获得以下输出:

Bonjour,mon nom est John
Hallo,mein Name ist John
你好,我叫约翰

请注意,这意味着名称遵循每种语言的本地化字符串。如果不是这种情况,则需要将占位符添加到资源字符串并修改上面的函数,或者需要查询函数中的文化信息并提供包含不同情况的 switch case 语句。有关资源文件的更多详细信息,请参阅如何在 C#中使用本地化

如果翻译不可用,最好使用大多数人都能理解的默认后备语言。我建议使用英语作为默认的后备语言。

递归插值

虽然不是很有用,但允许在另一个大括号内递归使用插值 string

Console.WriteLine($"String has {$"My class is called {nameof(MyClass)}.".Length} chars:");
Console.WriteLine($"My class is called {nameof(MyClass)}.");

输出:

字符串有 27 个字符:

我的类叫做 MyClass。