C# 11 新功能 -- 字串

近幾次來的 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)「換行」這兩個字必須要貼在編輯器的左方,任何多餘的空格都會被視為將要顯示的空格。

基於以上兩點,程式碼在閱讀上就沒有這麼直覺可以看出字串會顯示的排列方式。

而「原始字串常值文字」則可以靠著後方的三個雙引號來當作對齊標的,也就是說會以最後一行的三個雙引號的開頭為行開頭標示點,當採用此種格式的時候,目前任何內部字串都不能比這三個雙引號更前面,否則會引發錯誤。

Visual Studio 會標示出行開頭的位置

註:上述的需求當然還有另外的做法,像是使用字串插值:

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