我們不需要字串型別

周昌鴻發表於2013-12-01

字串是應該作為內建型別還是僅僅作為字元陣列的一個別名呢?考慮到實現細節的可選性,我並不認為需要對字串進行型別特化。在C++看來,字串和“vector”容器基本上是一樣的,除了某些特殊操作,例如:大小寫轉換,需要依賴容器元素“char”型別(而不是作用於容器本身)。

 

什麼是字串?

字串除了是一系列的字元組成,沒有什麼特別的。更確切地說,程式中的字串是特定字符集的字符集合。這裡的字元並不總是圖象字元,它可以包含可列印字元,連線字元,或者控制字元,那麼這又有什麼不同嗎?

考慮計算字串的長度,它應該返回的是字串中字形字元、連線符號的總數還是應該返回字串中字元佔用空間的長度?兩個字面等價但是內部儲存不同的的字串應該返回相同的長度嗎?考慮字串規範化的複雜程度和應用相同規則來計算‘length’長度聽起來很荒唐可笑。不同場景下字串的長度很難統一計算,而且這還依賴於字元渲染引擎。唯一有意義的是返回字串儲存空間大小——而這和計算字元陣列的長度是一致的。

我們可以通過對字串進行索引和取下標操作。那麼我們應該使用字元字面索引還是字元儲存索引?另外,考慮到組合Unicode字串,沒有統一的標準來衡量哪些字元是字面顯示字元,哪些是控制字元。字元的組合種類很多(不受限制),因此沒有固定的字元型別來定義一個“邏輯”字元。因此字串的操作應該針對儲存字元地址——這又和字元陣列沒有區別了。

 

C++中的不同

C++中的string和vector只有一個明顯的區別是:string是以null結尾的。並且string提供c_str方法返回內部字串儲存指標。(C++11定義了string來表明這是儲存字串的有效方式)。

對C++來說,如果string不提供c_str方法,那麼string類就基本沒有存在的必要(相對於vector)。然而,這是也不是一個必須的特性,提供出來只是為了方便將string進行轉換,從而方便呼叫早期C風格的字串指標API。怪異的是,C++標準庫也使用了類似的介面,ofstream的建構函式需要傳遞的是一個‘const char*’指標而不是string型別。(在C++11中修復了該問題)

使用null作為字串結束符也是一個糟糕的選擇,導致在C函式庫中,一些函式如:strcat, strcpy並不安全。使用C風格的字串是一件令人生畏且容易出錯的差事。現代風格的API介面已經很少依賴使用null作為字串結束,這些函式通常都要求提供字串的長度作為一個引數。

導致C++中的string和vector有所不同,這是因為歷史包袱,而大多數程式設計師都不需要關心它的存在。

 

字元代理和變長字元編碼

前述的討論基於這樣一個假設:字串儲存中一個儲存元素編碼一個字元。而通常採用這種編碼方式是效率低下的,使用變長字元編碼可以解決這個問題:使用不同數量的位元組來表示一個字元。例如:在UTF-16中,一個字元可以由2個位元組或者4個位元組進行儲存。而不儲存字元的單元作為儲存序列的一部分,被叫做字元代理。

字元代理和連線字元不同。字元代理在字符集中沒有意義:它只是用來填充編碼佔位。對字元的到操作依賴以實際儲存元素位置。將編碼字串當作一系列的字元來操作,通常很麻煩而且容易導致未知語義。編碼字串的length應該返回什麼值呢?是編碼字元的個數還是儲存元素的個數?

目前的方法是,將編碼字串作為一種特殊字元型別,並提供一定程度的抽象,你可要儲存各種型別的字元,也可以將其作為一個字元序列操作。length返回字元的個數,而與底層編碼方式無關(或者通過其他方法返回)。

設計這個類的挑戰在於效率。基本操作如索引字元變成了一個線性複雜度操作。需要先對字串進行解碼,從開始掃描,重新組織字元代理,並計算真實的字元個數。即便是簡單的前向掃描也依賴以迴圈和下一個字元狀態的解析。而這種操作負載在所有的基本操作都會被累積,比如拆分,翻譯和正規表示式匹配。

目前(內建字串型別)的語言並沒有按這種方式操作字串,考慮效率問題,這使得采用這種方式變得沒有吸引力。而在字元域,載入字元,解碼字元和對字串進行處理則簡單得多。這種方式會消耗更多記憶體,但是我認為這對於世界上現有的字符集來說並不是一個主要問題——儘管存在大量的字符集,但是相較其他集合則小得多。

 

函式庫的支援

字串有許多相對於簡單陣列的特殊操作:規範化、字元轉換、正規表示式運算、字元解析、格式化、裁剪、編碼等等。相對地,任何型別的“vector”都有一些特殊操作:數字可以累加,求平均,計算中位數。向量可以做變換,簡化和柵格化。

值得探討的是,一些集合運算是應該作為一個成員函式還是獨立函式。如果上述操作作為一個成員函式,那麼需要特地提供一個”string”型別。而上述操作提供為獨立函式的話,使用原始”array”陣列型別就可以工作了。顯然,上述操作都可以寫成獨立函式,沒有那個函式需要特別的處理,只需要提供array介面就可以了。

不過有一個語法上的特例,如果我把”str.toUpperCase()”提取為獨立函式顯得有點怪異。D語言則完全統一了函式呼叫語法。我預計C++也會跟隨這一趨勢,許多操作函式已經被當作獨立函式提供而不是採用成員函式。似乎發展也傾向於獨立函式。

如果獨立函式可行,那麼就沒有必要提取一個string類。字串操作可以寫成作用於字元陣列的獨立函式。

 

字串代表什麼?

如果你的字串不僅僅使用ASCI字元,通常可以考慮使用Unicode字符集,但是,也有可能在你的程式碼中,只使用了ascii碼或者是latin-1編碼。不過大多數字符串都不會僅限於此。有些語言,比如PHP,允許你在全域性範圍設定編碼方式。使用string做標記,通常都不會做太多變化(譯:我猜測作者的意思應該是大多數語言已經內建了string的編碼方式,而且不允許調整)。

我們假定字符集使用ascii編碼,以ascii編碼的字串使用一個模版類string。為了標識不同於其他字符集,我們把這個字串型別標識為“char ascii”。一旦我們做了這樣的設定,我們就不希望再感受到string的型別了,它工作起來就和陣列很相似了。

回到剛才談到的變長編碼:如果你使用UTF-16編碼字元,並且需要使用字元代理。現在string變得含義模糊了,string應該被當做unicode字元組合還是真實的utf16編碼值?(沿用剛才的實現)使用一個型別別名標識比較合適,我們假定為“type utf16:binary 16bit”,並作為一個陣列。現在歧義消除了,字串是編碼值的集合,而不是字元。

 

僅僅是一個typedef?

現在我覺得不需要定義一個特殊的string型別,如果需要string型別,可以僅僅使用一個陣列的別名。但是string作為基礎型別被大量使用,這也導致了許多問題。一些情況下需要著重考慮字元編碼,使用特定型別的陣列就會比較安全。同時也需要一個富字串處理函式庫,但是不應該作為一個字串型別(string)提供。

你能舉例有什麼情況下需要專門的string型別,或者這樣做會更有效嗎?

相關文章