字串插值

字串插值允許開發人員將 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。