字符串插值
字符串插值允许开发人员将 variables
和 text 组合在一起形成一个字符串。
基本例子
创建了两个 int
变量:foo
和 bar
。
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。
表达式
使用字符串插值,也可以计算花括号 {}
中的表达式。结果将插入字符串中的相应位置。例如,要计算 foo
和 bar
的最大值并插入它,请在花括号内使用 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.")
注意 :Current
和 Invariant
不能创建为扩展方法,因为默认情况下,编译器将类型 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
相反,创建一个消耗变量并返回 String
的 Func<>
:
public static Func<Exception, string> FormatError =
error => $"Exception caught:\r\n{error}";
// ...
Logger.Log(FormatError(ex));
字符串插值和本地化
如果你正在本地化你的应用程序,你可能想知道是否可以使用字符串插值和本地化。确实,有可能存储资源文件 String
s,如:
"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。