Swift String 為什麼故意把 Index 設計的這麼難用 - Relearning Note

Terry Huang
安郡尼爾筆記
Published in
7 min readNov 27, 2020

「你解釋解釋,為什麼字串無法直接以數字 index 取值、做 Subscript」

Unicode 歷史了解一下

Unicode,中文又稱萬國碼、國際碼、統一碼、單一碼,是電腦科學領域裡的一項業界標準。它對世界上大部分的文字系統進行了整理、編碼,使得電腦可以用更為簡單的方式來呈現和處理文字。

早期電腦以一個 8 位元來代表一個文字狀態,以其中 7 個位元值 0~127 來代表空格、英文大小寫、數字、符號等等,也就是 ASCII 編碼(American Standard Code for Information Interchange)。

但電腦傳至多個國家之後,各國便將其文字編入其中,但 128~255 的空間位置也有限,因此也衍伸出了多位元組的編碼方式。比如台灣早前電腦的中文系統,如:倚天中文,大五碼…;而簡體中文也發展出 GB2312、GBK…等等。

為了解決各國編碼不同而造成的問題,國際化標準組織 (ISO) 決定解決這個問題,建立 ISO/IEC 推出了通用字元集 UCS (Universal Character Set / Universal Multiple-Octet Coded Character Set)。同時期也有軟體商 Xerox、Apple…等等嘗試建造單一字元集統一碼 (Unicode)。

後來雙方達成一致,合併其開發成果。同意保持兩者標準的碼表相容,並緊密地共同調整任何未來的擴充。

傳統的 String 設計

以 C++ 語言為例,最傳統的 std::string 以 char 組成,一個 char 一個 byte;在 Java 中一個 char 則為 2 byte。

這樣設計的好處是,每隔字元使用的記憶體是固定的,因此也可以透過簡單的加減來計算出特定字元的記憶體位置。但缺點就非常明顯了,當字串用來處理多位元字符時,便會發生 char 長度與可視的長度不同。

Swift 與 Unicode Sclar

以現有的 Unicode 規劃來說,涵蓋 U+0000 到 U+10FFFF,共有1,112,064個碼位(code point),以常見的 UInt32 (4 byte)來紀錄,便能代表一個任意數值的碼位。

而在 Swift 中也確實有 Unicode Scalar 型別能紀錄任意的單一 Unicode Code Point。所以,如果以此為單位來規劃「字串」與「字元」系統,不就能設計出完美的解決方案了嗎?但 Swift 中 String 卻還不是由 Scalar 組成,到底是為什麼?

“因為應該視為一個 Unit 處理的單元,可能由多個 Unicode Code Point 組成!沒錯,因為有個東西叫做 Grapheme Cluster”

Grapheme Cluster 駕到

早在 Unicode 3.2 中,便引入了 Grapheme Cluster,能將數個 Unicode Code Point 組成一個 Unit,這樣一個 Unit 在使用者情境下應視為一個單位「字」處理。以下為最常見的一些用法:

  • 組合韓文,比如:한,可由 3 個 Code Point 組成(ᄒ, ᅡ, ᆫ)
  • 組合字元,比如:ò,是由 2 個 Code Point 組成(o, ̀)
  • 修飾 Emoji,比如:🚴🏾,是由 2 個 Code Point 組成( 🚴, 🏾)

Swift 的字與字串

在追求極致的處理環境下,在 Swift 中定義了一個完美的「邏輯環境」:應該被當為單一 Unit 處理的「字」型別為 Character,一個 Character(字)由一個或多個 Unicode Scalar 組成,而字串 String 則由多個 Character 組成。

The `Character` type represents a character made up of one or more Unicode scalar values, grouped by a Unicode boundary algorithm. Generally, a `Character` instance matches what the reader of a string will perceive as a single character. Strings are collections of `Character` instances, so the number of visible characters is generally the most natural way to count the length of a string.

由以下的範例來看,「한Dòg🚴🏾」這在文字上可簡易辨識成 5 個 Unit,在 Swift String 中完全能夠正確的 Count 出 5 的長度,但其實是由 9 個 Scalar 組成,也能以 11 個 UTF-16 或是 22 個 UTF-8 表示。

題外話,就連 Swift 語言本身都是極端的 Unicode Friendly,您甚至可以用來命名變數。(相信大家已經見怪不怪)

毀天滅地的複雜度

也就因為了 String 中每個 Character(字)可能由多個 Unicode Scalar 組成,因此並無法簡單地從記憶體位置的差距,來計算出某個 Character(字)的位置。因此,當你需要第 5 個 Character(字)時,Swift 需要從 String 的 startIndex 往後逐字跳躍,直到完成後才能取得正確的資料。換句話說,以任意數字為 Index 取得 Character(字)的複雜度遠比想像中的高。

因此,針對 Swift String 字串相關 Protocol 設計,在後期便直接把數字取字元的概念移除,以更多元化的 Index 進退與設計,來滿足字串處理的需求。

P.S. 其實在 Swift 2~3 的版本時,曾經是棄用 Index 支援 Int 的,但在 4 版之後又回歸了 Index 設計。

當然,網路上流通了非常多的 String Extension 語法糖,能夠讓使用者簡單的回到從前,但這一切剛好也就抹滅了這個故意設計的用意。也就只有在瞭解了這背後的設計原因,才更能知道如何讓程式進一步的優化。

P.S. 如果對於 String.Index 設計有興趣,也能參考原始檔案中的說明

Reference

--

--

Terry Huang
安郡尼爾筆記

Co-Founder of LiRise Co.,Ltd. In charge of innovative affairs development. Tags: Guitar, Golf, Photograph, Cocktail, Dance, Diving, Travel.