--- comments: true --- # 3.4   字元編碼 * 在計算機中,所有資料都是以二進位制數的形式儲存的,字元 `char` 也不例外。為了表示字元,我們需要建立一套“字符集”,規定每個字元和二進位制數之間的一一對應關係。有了字符集之後,計算機就可以透過查表完成二進位制數到字元的轉換。 ## 3.4.1   ASCII 字符集 ASCII 碼是最早出現的字符集,其全稱為 American Standard Code for Information Interchange(美國標準資訊交換程式碼)。它使用 7 位二進位制數(一個位元組的低 7 位)表示一個字元,最多能夠表示 128 個不同的字元。如圖 3-6 所示,ASCII 碼包括英文字母的大小寫、數字 0 ~ 9、一些標點符號,以及一些控制字元(如換行符和製表符)。 ![ASCII 碼](character_encoding.assets/ascii_table.png){ class="animation-figure" }

圖 3-6   ASCII 碼

然而,**ASCII 碼僅能夠表示英文**。隨著計算機的全球化,誕生了一種能夠表示更多語言的 EASCII 字符集。它在 ASCII 的 7 位基礎上擴展到 8 位,能夠表示 256 個不同的字元。 在世界範圍內,陸續出現了一批適用於不同地區的 EASCII 字符集。這些字符集的前 128 個字元統一為 ASCII 碼,後 128 個字元定義不同,以適應不同語言的需求。 ## 3.4.2   GBK 字符集 後來人們發現,**EASCII 碼仍然無法滿足許多語言的字元數量要求**。比如漢字有近十萬個,光日常使用的就有幾千個。中國國家標準總局於 1980 年釋出了 GB2312 字符集,其收錄了 6763 個漢字,基本滿足了漢字的計算機處理需要。 然而,GB2312 無法處理部分罕見字和繁體字。GBK 字符集是在 GB2312 的基礎上擴展得到的,它共收錄了 21886 個漢字。在 GBK 的編碼方案中,ASCII 字元使用一個位元組表示,漢字使用兩個位元組表示。 ## 3.4.3   Unicode 字符集 隨著計算機技術的蓬勃發展,字符集與編碼標準百花齊放,而這帶來了許多問題。一方面,這些字符集一般只定義了特定語言的字元,無法在多語言環境下正常工作。另一方面,同一種語言存在多種字符集標準,如果兩臺計算機使用的是不同的編碼標準,則在資訊傳遞時就會出現亂碼。 那個時代的研究人員就在想:**如果推出一個足夠完整的字符集,將世界範圍內的所有語言和符號都收錄其中,不就可以解決跨語言環境和亂碼問題了嗎**?在這種想法的驅動下,一個大而全的字符集 Unicode 應運而生。 Unicode 的中文名稱為“統一碼”,理論上能容納 100 多萬個字元。它致力於將全球範圍內的字元納入統一的字符集之中,提供一種通用的字符集來處理和顯示各種語言文字,減少因為編碼標準不同而產生的亂碼問題。 自 1991 年釋出以來,Unicode 不斷擴充新的語言與字元。截至 2022 年 9 月,Unicode 已經包含 149186 個字元,包括各種語言的字元、符號甚至表情符號等。在龐大的 Unicode 字符集中,常用的字元佔用 2 位元組,有些生僻的字元佔用 3 位元組甚至 4 位元組。 Unicode 是一種通用字符集,本質上是給每個字元分配一個編號(稱為“碼點”),**但它並沒有規定在計算機中如何儲存這些字元碼點**。我們不禁會問:當多種長度的 Unicode 碼點同時出現在一個文字中時,系統如何解析字元?例如給定一個長度為 2 位元組的編碼,系統如何確認它是一個 2 位元組的字元還是兩個 1 位元組的字元? 對於以上問題,**一種直接的解決方案是將所有字元儲存為等長的編碼**。如圖 3-7 所示,“Hello”中的每個字元佔用 1 位元組,“演算法”中的每個字元佔用 2 位元組。我們可以透過高位填 0 將“Hello 演算法”中的所有字元都編碼為 2 位元組長度。這樣系統就可以每隔 2 位元組解析一個字元,恢復這個短語的內容了。 ![Unicode 編碼示例](character_encoding.assets/unicode_hello_algo.png){ class="animation-figure" }

圖 3-7   Unicode 編碼示例

然而 ASCII 碼已經向我們證明,編碼英文只需 1 位元組。若採用上述方案,英文文字佔用空間的大小將會是 ASCII 編碼下的兩倍,非常浪費記憶體空間。因此,我們需要一種更加高效的 Unicode 編碼方法。 ## 3.4.4   UTF-8 編碼 目前,UTF-8 已成為國際上使用最廣泛的 Unicode 編碼方法。**它是一種可變長度的編碼**,使用 1 到 4 位元組來表示一個字元,根據字元的複雜性而變。ASCII 字元只需 1 位元組,拉丁字母和希臘字母需要 2 位元組,常用的中文字元需要 3 位元組,其他的一些生僻字元需要 4 位元組。 UTF-8 的編碼規則並不複雜,分為以下兩種情況。 - 對於長度為 1 位元組的字元,將最高位設定為 $0$ ,其餘 7 位設定為 Unicode 碼點。值得注意的是,ASCII 字元在 Unicode 字符集中佔據了前 128 個碼點。也就是說,**UTF-8 編碼可以向下相容 ASCII 碼**。這意味著我們可以使用 UTF-8 來解析年代久遠的 ASCII 碼文字。 - 對於長度為 $n$ 位元組的字元(其中 $n > 1$),將首個位元組的高 $n$ 位都設定為 $1$ ,第 $n + 1$ 位設定為 $0$ ;從第二個位元組開始,將每個位元組的高 2 位都設定為 $10$ ;其餘所有位用於填充字元的 Unicode 碼點。 圖 3-8 展示了“Hello演算法”對應的 UTF-8 編碼。觀察發現,由於最高 $n$ 位都設定為 $1$ ,因此系統可以透過讀取最高位 $1$ 的個數來解析出字元的長度為 $n$ 。 但為什麼要將其餘所有位元組的高 2 位都設定為 $10$ 呢?實際上,這個 $10$ 能夠起到校驗符的作用。假設系統從一個錯誤的位元組開始解析文字,位元組頭部的 $10$ 能夠幫助系統快速判斷出異常。 之所以將 $10$ 當作校驗符,是因為在 UTF-8 編碼規則下,不可能有字元的最高兩位是 $10$ 。這個結論可以用反證法來證明:假設一個字元的最高兩位是 $10$ ,說明該字元的長度為 $1$ ,對應 ASCII 碼。而 ASCII 碼的最高位應該是 $0$ ,與假設矛盾。 ![UTF-8 編碼示例](character_encoding.assets/utf-8_hello_algo.png){ class="animation-figure" }

圖 3-8   UTF-8 編碼示例

除了 UTF-8 之外,常見的編碼方式還包括以下兩種。 - **UTF-16 編碼**:使用 2 或 4 位元組來表示一個字元。所有的 ASCII 字元和常用的非英文字元,都用 2 位元組表示;少數字符需要用到 4 位元組表示。對於 2 位元組的字元,UTF-16 編碼與 Unicode 碼點相等。 - **UTF-32 編碼**:每個字元都使用 4 位元組。這意味著 UTF-32 比 UTF-8 和 UTF-16 更佔用空間,特別是對於 ASCII 字元佔比較高的文字。 從儲存空間佔用的角度看,使用 UTF-8 表示英文字元非常高效,因為它僅需 1 位元組;使用 UTF-16 編碼某些非英文字元(例如中文)會更加高效,因為它僅需 2 位元組,而 UTF-8 可能需要 3 位元組。 從相容性的角度看,UTF-8 的通用性最佳,許多工具和庫優先支持 UTF-8 。 ## 3.4.5   程式語言的字元編碼 對於以往的大多數程式語言,程式執行中的字串都採用 UTF-16 或 UTF-32 這類等長編碼。在等長編碼下,我們可以將字串看作陣列來處理,這種做法具有以下優點。 - **隨機訪問**:UTF-16 編碼的字串可以很容易地進行隨機訪問。UTF-8 是一種變長編碼,要想找到第 $i$ 個字元,我們需要從字串的開始處走訪到第 $i$ 個字元,這需要 $O(n)$ 的時間。 - **字元計數**:與隨機訪問類似,計算 UTF-16 編碼的字串的長度也是 $O(1)$ 的操作。但是,計算 UTF-8 編碼的字串的長度需要走訪整個字串。 - **字串操作**:在 UTF-16 編碼的字串上,很多字串操作(如分割、連線、插入、刪除等)更容易進行。在 UTF-8 編碼的字串上,進行這些操作通常需要額外的計算,以確保不會產生無效的 UTF-8 編碼。 實際上,程式語言的字元編碼方案設計是一個很有趣的話題,涉及許多因素。 - Java 的 `String` 型別使用 UTF-16 編碼,每個字元佔用 2 位元組。這是因為 Java 語言設計之初,人們認為 16 位足以表示所有可能的字元。然而,這是一個不正確的判斷。後來 Unicode 規範擴展到了超過 16 位,所以 Java 中的字元現在可能由一對 16 位的值(稱為“代理對”)表示。 - JavaScript 和 TypeScript 的字串使用 UTF-16 編碼的原因與 Java 類似。當 1995 年 Netscape 公司首次推出 JavaScript 語言時,Unicode 還處於發展早期,那時候使用 16 位的編碼就足以表示所有的 Unicode 字元了。 - C# 使用 UTF-16 編碼,主要是因為 .NET 平臺是由 Microsoft 設計的,而 Microsoft 的很多技術(包括 Windows 作業系統)都廣泛使用 UTF-16 編碼。 由於以上程式語言對字元數量的低估,它們不得不採取“代理對”的方式來表示超過 16 位長度的 Unicode 字元。這是一個不得已為之的無奈之舉。一方面,包含代理對的字串中,一個字元可能佔用 2 位元組或 4 位元組,從而喪失了等長編碼的優勢。另一方面,處理代理對需要額外增加程式碼,這提高了程式設計的複雜性和除錯難度。 出於以上原因,部分程式語言提出了一些不同的編碼方案。 - Python 中的 `str` 使用 Unicode 編碼,並採用一種靈活的字串表示,儲存的字元長度取決於字串中最大的 Unicode 碼點。若字串中全部是 ASCII 字元,則每個字元佔用 1 位元組;如果有字元超出了 ASCII 範圍,但全部在基本多語言平面(BMP)內,則每個字元佔用 2 位元組;如果有超出 BMP 的字元,則每個字元佔用 4 位元組。 - Go 語言的 `string` 型別在內部使用 UTF-8 編碼。Go 語言還提供了 `rune` 型別,它用於表示單個 Unicode 碼點。 - Rust 語言的 `str` 和 `String` 型別在內部使用 UTF-8 編碼。Rust 也提供了 `char` 型別,用於表示單個 Unicode 碼點。 需要注意的是,以上討論的都是字串在程式語言中的儲存方式,**這和字串如何在檔案中儲存或在網路中傳輸是不同的問題**。在檔案儲存或網路傳輸中,我們通常會將字串編碼為 UTF-8 格式,以達到最優的相容性和空間效率。