近幾次來的 C# 改版都在字串出了很多新花樣,C# 11 也來了這麼幾個,這篇簡單來看一下這幾個新功能的介紹。
Raw string literals
這個功能中文被稱為 「原始字串常值文字」,透過至少三對雙引號使用,你沒看錯是三對。之前有個大家很常用的功能是採用 @ 表示的「逐字字串常值」(或稱逐字解譯) 已經挺好用的了,現在又來個「原始字串常值文字」。我們可以概括的把「原始字串常值文字」視為是 「逐字字串常值」的一種加強版。
我們先來看一個例子,比方說想要列出一段這樣的文字:這有雙雙引號 ""在雙雙引號內""。
三代的寫法如何做?
string value1 = "這有雙雙引號 \"\"在雙雙引號內\"\"";
Console.WriteLine(value1);
string value2 = @"這有雙雙引號 """"在雙雙引號內""""。";
Console.WriteLine(value2);
string value3 = """這有雙雙引號 ""在雙雙引號內""。""";
Console.WriteLine(value3);
第一代寫法就要用跳脫字元;第二代寫法則是會把要顯示的雙引號變成兩倍;第三代則是維持要顯示的兩對雙引號。在我的感覺上二代和三代其實差不多,除非一個字串的內部會有一卡車的雙引號要顯示。
接著來瞧瞧字串換行,想要顯示:
長字串
換行
string longString1 = "長字串" + Environment.NewLine+ "換行";
Console.WriteLine(longString1);
string longString2 = @"長字串
換行";
Console.WriteLine(longString2);
string longString3="""
長字串
換行
""";
Console.WriteLine(longString3);
在換行這點上,用「逐字字串常值」會有一些麻煩的點,
(a)第一個字要接在雙引號後面,不能跳到下一行,比方這樣:
string longString2 = @"
長字串
換行";
他會在最上方多丟一行空白行出來。
(b)「換行」這兩個字必須要貼在編輯器的左方,任何多餘的空格都會被視為將要顯示的空格。
基於以上兩點,程式碼在閱讀上就沒有這麼直覺可以看出字串會顯示的排列方式。
而「原始字串常值文字」則可以靠著後方的三個雙引號來當作對齊標的,也就是說會以最後一行的三個雙引號的開頭為行開頭標示點,當採用此種格式的時候,目前任何內部字串都不能比這三個雙引號更前面,否則會引發錯誤。
註:上述的需求當然還有另外的做法,像是使用字串插值:
string longString4 = $"長字串{Environment.NewLine}換行";
搭配字串插值,一般的狀況下沒甚麼特別:
int age = 10;
string name = "小明";
string description1 = $""" {name} 的年齡是 {age} """;
像上面這樣的程式碼我們應該都知道輸出會長這樣:小明 的年齡是 10。
如果想加上 {} 呢?那就需要搭配多一點的 $ 符號,例如:
string description2 = $$"""{{{name}} 的年齡是 {{age}}}。""";
會得到這樣的輸出:{小明 的年齡是 10}。
如果要兩個大括弧呢?像是:{{小明 的年齡是 10}}。
string description3 = $$$"""{{{{{name}}} 的年齡是 {{{age}}}}}。""";
一團亂,越來越多的 $ 和 {},那這個數量是怎麼算的,這個數量的算法是 $ 的數量代表字串插值的 { } 數量,以上面為例,前方有三個 $ 符號,代表{{{name}}} 和 {{{age}}} 是字串插值,而在 {{{name}}} 前方的 {{ 和 {{{age}}} 後方的}} 則被視為是字串內容不需解譯。另外還需要符合一個規則是被視為字串內容的每一組{}數量不得超過 $ 符號數量,也就是說當 $ 是三個的時候,最多只能是 {{ }},注意是每一組,而不是總和數,可以觀察下列的程式碼比對:
string description4 = $$$"""{{{{{name}}}}} 的年齡是 {{{{{age}}}}}。""";
上方程式碼的結果是:{{小明}} 的年齡是 {{10}}。我第一次看到類似的範例的時候頭大的不得了,仔細算算才釐清他的規則。
UTF-8 string literals
在 web application 的世界裡,時常會應用到 UTF-8 編碼,在過去我們通常會這樣取得 UTF-8 編碼的 bytes 陣列:
var value = "魯夫";
byte[] array = Encoding.UTF8.GetBytes(value);
現在你可以利用 u8 後綴字這樣寫:
ReadOnlySpan<byte> span = "魯夫"u8;
不過這個僅能使用在字串常值。
Pattern match Span<char>
這個功能主要是讓 Span<char> 或 ReadOnlySpan<char> 能夠直接和字串模式比對,例如:
static bool Is123(ReadOnlySpan<char> s)
{
return s is "123";
}
我遇過一個情境,正好很適合應用 – 固定長度分隔文字檔檢查,比方說有一個這樣的字串 4569008000,需要檢查前三個字元是否符合 456。
來模擬演練一下,我們有以下幾行字串需要辨識開頭是否符合 456:
static void Main(string[] args)
{
var list = new List<string>
{
"45600001",
"45600002",
"45700003",
"45600004",
"45700005",
};
foreach (var item in list)
{
Console.WriteLine($"by span {item} is {Is456BySpan(item)}");
Console.WriteLine($"by string {item} is {Is456ByString(item)}");
};
}
static bool Is456BySpan(ReadOnlySpan<char> source)
{
return source.Slice(0,3) is "456";
}
static bool Is456ByString(string source)
{
return source.Substring(0,3) is "456";
}
過去可能會採用的方式就是 Is456ByString,也就是透過 Substring 方法先將來源切割後比對,其實這也沒啥不好,唯一的問題就是 Substring 會在 heap 多出一個字串執行個體。
但如果用 Span<char> 或 ReaOnlySpan<char> 的 Slice 方法就沒有這個困擾了。
以上關於 C# 11 有關的字串新功能就介紹到這邊。相關的範例在我的 github。