i 9 NSString和Unicode

在當(dāng)前這個時代(比如說公元2016年),如果你并不是在維護(hù)歷史遺留的文本處理代碼,沒有在每個地方都使用Unicode的話,文本處理會出錯。幸運的是Apple和NeXT促成了旨在將全世界所有字符都納入同一種編碼體系即Unicode標(biāo)準(zhǔn)的制定,也正是在1994年NeXT的foundation Kit成為了史上基于Unicode且面向任何編程語言的標(biāo)準(zhǔn)庫。但即便NSString完全支持Unicode且提供了豐富的文本實用功能,處理數(shù)百種語言的文本仍然是一個相當(dāng)棘手的事情,而且NSString的個中細(xì)節(jié)也是作為程序員的你需要仔細(xì)參詳?shù)摹?br> 本文分為兩部分,第一部分是關(guān)于Unicode編碼,第二部分是關(guān)于Unicode下的NSString和使用NSString的時候會遇到的問題

Unicode的前世今生

眾所周知,計算機(jī)是無法直接處理文本(即一串字符)的,它們只能處理數(shù)字。所以計算機(jī)中是使用一串?dāng)?shù)字來表示文本的,而實現(xiàn)數(shù)字到文本的映射即稱之為編碼。
最為人熟知的編碼即是ASCII碼,它是一個7位的編碼,將英文字母,數(shù)字,標(biāo)點符號和控制字符映射到0到127之間的整數(shù)。隨后出現(xiàn)了很多不同的8位編碼以將除了英文之外的字符使用到計算機(jī)中,它們大多基于ASCII,即使用了未使用的第8位以編碼其它的字母,符號或者整個字母表(西里爾字符和希臘字符)
這些編碼互相之間是不兼容的,而且8位空間仍然不足以承載即使是歐洲所使用的所有字符,更別說世界范圍內(nèi)其它的字符了。當(dāng)時基于文本的計算機(jī)系統(tǒng)的一個問題是同一時刻只能使用一種編碼(也稱為code page)。如果在一臺機(jī)器上寫文本,在另一臺使用不同編碼頁的機(jī)器上打開時,所有128到255對應(yīng)的字符都會解析錯誤。
東亞的字符比如中文,日文,朝鮮文還有另一個問題,那就是CJK所包含的字符如此之多,8位根本不夠編碼,所以催生了16位字符編碼的出現(xiàn)。而一旦在處理不止一個字節(jié)能滿足的值的時候,如何將它們存儲在內(nèi)存和磁盤上就不是一個小問題了。你需要根據(jù)字節(jié)序定義第二種映射規(guī)則,或者使用變長編碼代替等寬編碼。需要注意的是這第二種映射其實也是一種形式的編碼,而我們用了同一個詞“編碼”其實是使大家對編碼這一概念產(chǎn)生混淆的基本來源,下面講UTF8和UTF16的時候會再進(jìn)行討論。
現(xiàn)代操作系統(tǒng)大多都已經(jīng)擺脫同一時刻只能使用一個code page的束縛了,所以只要每個文檔正確地報告它的編碼,處理數(shù)百種不同的編碼都是可能的,不可能的是在同一個文檔中使用多種編碼,即在同一文檔中用多種語言編寫,而也就是這個原因為前Unicode時代的棺木訂上了最后一個釘子。
1987年開始,世界上主要的技術(shù)公司,包括蘋果和NeXT開始一起開發(fā)一種容納全世界所有文字系統(tǒng)的字符編碼,并促成了Unicode標(biāo)準(zhǔn)1.0.0于1991年10月的發(fā)布。

Unicode概覽

基本情況

在基本層面上,Unicode標(biāo)準(zhǔn)為世界上幾乎所有的印刷系統(tǒng)中所有字符和符號都定義了唯一的數(shù)值,每個這樣的數(shù)據(jù)都稱為code point,表示為U+xxxx,xxxx為4到6個16進(jìn)制數(shù)。比如code point U+0041在拉丁字母表中代表A,而U+1F61B代表名為FACE WITH STUCK-OUT TONGUE的emoji ??,需要注意的是字符的名字也是Unicode標(biāo)準(zhǔn)的一部分??梢杂?a target="_blank" rel="nofollow">標(biāo)準(zhǔn)代碼表或者使用OS X上的

charater viewer
即圖中的顯示字符顯示程序查找code point,在我10.10.5的系統(tǒng)上,未能在選中某個字符比如表情之后在右邊顯示其Unicode編碼,可以通過右擊表情“拷貝字符簡介”并找地方粘貼出來以顯示其編碼。
如上述提到的其它編碼一樣,Unicode只是抽象地描述了字符,定義了字符對應(yīng)的值和名字等信息,但并沒有規(guī)定它顯示的glyph是怎樣。比如對于中日朝文中都使用的某個漢字,Unicode定義了相同的code point但在這三種語言印刷系統(tǒng)中,這個漢字的顯示glyph并不一定是相同的,因為它們是在各自國家的印刷系統(tǒng)中各自定義的。
Unicode最初是16位的編碼,可定義65536個字符,被認(rèn)為可以容納下世界范圍內(nèi)當(dāng)今的所有字符,廢棄及很少使用的字符被認(rèn)為是需要放到Private use Areas的,即65536中的某一段保留區(qū)域U+E000–U+F8FF(當(dāng)然,由于Unicode其實是一個21位的編碼,所以還有BMP之外的兩段private use area,即plane15和16中的U+F0000–U+FFFFD, U+100000–U+10FFFD),用戶可以將這段區(qū)域的code point映射到自己定義的字符中,所以對于不同的定義,code point是會沖突的。Apple在這段區(qū)域中定義了一些符號和控制字符,雖然大多數(shù)都已經(jīng)不推薦使用,一個著名的例外是apple的logo字符U+F8FF: ?,如果你此刻并不是使用apple平臺的機(jī)器,則這個字符的顯示就會根據(jù)你機(jī)器的平臺而顯示一個完全不同的字符。
Unicode編碼空間隨后就擴(kuò)展成了21位,以容納歷史上使用或者很少使用的?日本漢字或者中文字符。這是一個很重要的點,而且之后即將介紹的NSString也與它很相關(guān),那就是Unicode并不是16位編碼。21位編碼提供了1,114,112 code point。目前只使用了10%左右的空間,所以還有大把的code point可用。
Unicode編碼空間劃分為17個65536空間,每個65536空間稱為一個plane,Plane 0稱為Basic Multilingual Plane (BMP),也是目前所使用的絕大多數(shù)字符所在的Plane,除了著名的emoji,其它的plane稱為增補planes,且大部分都是空的。

特有的Unicode特征

其實將Unicode當(dāng)成所有現(xiàn)存編碼(大部分是8位的)的聯(lián)合而不是一個universal code會更恰當(dāng),大多是為了兼容歷史遺留的編碼的原因,Unicode編碼標(biāo)準(zhǔn)中包含了很多在處理Unicode字符串時需要注意的細(xì)微之處。

結(jié)合字符序列

為了與之前的編碼兼容,有些字符不僅可以看成是單個code point,也可以看成是多個code point的序列。比如é既可以是precomposed 字符U+00E9 (LATIN SMALL LETTER E WITH ACUTE),也可以看成是分解的?形式U+0065 (LATIN SMALL LETTER E)在前U+0301 (COMBINING ACUTE ACCENT)在后。這兩種形式是combining(or composite)character sequence中的變體,有這兩種存在的原因其實是有些Unicode實現(xiàn)對precomposed字符glyph的處理好,而對分解形式的glyph可能無法辨認(rèn)所造成,又或者有的Unicode實現(xiàn)相反,這種情況所造成的。結(jié)合字符不僅在西方出現(xiàn)在,在東方比如Hangul,音節(jié)?可以由單個code point(U+AC00) 表示,也可以由? + ? (U+1100 U+1161)序列表示。
在Unicode用語中,這兩種形式并不是等價的,因為包含不同的code point,卻是canonically equivalent:即它們有相同的形態(tài)(按道理是看起來應(yīng)該是一樣的,但實際是不是一樣就得問上帝了)和意思。

重復(fù)字符

許多看起來相同的字符使用不同的code point編碼了多次,且代表了不同的意思。比如拉丁字母A在形狀上與西里爾字母A (U+0410)一樣,但它們其實是不同的。將它們當(dāng)成不同的code point不僅減輕了多遺留編碼中轉(zhuǎn)換的過程,也允許Unicode文本持有字符的意義。
當(dāng)然也有極少數(shù)的真正相等的重復(fù),非營利性的Unicode標(biāo)準(zhǔn)及發(fā)展組織Unicode Consortium所列的 字母? (LATIN CAPITAL LETTER A WITH RING ABOVE, U+00C5) 和字符 ? (ANGSTROM SIGN, U+212B),因為?ngstr?m實際是瑞典語的大寫字母,它們是完全相同的,在Unicode中它們不相等但是canonically相等。
很多字符和序列則在廣義上是重復(fù)的,在Unicode標(biāo)準(zhǔn)中稱為 compatibility equivalence ,兼容序列代表相同的抽象字符,但可能沒有相同的視覺體現(xiàn)或者行為。比如很多希臘字母,同時也用于數(shù)學(xué)了技術(shù)符號。羅馬數(shù)字在標(biāo)準(zhǔn)拉丁字母之外額外編碼在U+2160到 U+2183間。其它的兼容相等的例子是 ligatures:字符? (LATIN SMALL LIGATURE FF, U+FB00) 與序列 ff (LATIN SMALL LETTER F + LATIN SMALL LETTER F, U+0066 U+0066)兼容,但不是canonically equivalent,雖然它們可能渲染得完全一樣,但取決于不同的上下文,typeface及文本渲染系統(tǒng)的能力。

Normalization形式

已經(jīng)看到Unicode中字符串相等并不是一個簡單的概念,除了比較兩個字符串,一個個code point的比較,還要比較canonical equivalence 和compatibility equivalence。Unicode為此提供了為所有相等的序列生成一個獨一無二的code point序列的標(biāo)準(zhǔn)正交算法 。相等的標(biāo)準(zhǔn)既可以是canonical (NF) ?或者 compatibility (NFK),四種Unicode的正交形式及獲取此種正交形式的算法如下

Unicode正交算法.png

而NSString對上述4種正交算法的實現(xiàn)
NSString正交化實例方法

出于比較字符串的目的,既可以將它們?nèi)空换癁榉纸猓―)形式,也可以正交化為?組合(C)形式。通常分解形式更快,因為組合形式的算法包含兩個步驟:字符會先分解然后重新進(jìn)行組合。如果一個字符序列包含多個combining marks,combining marks的順序在分解之后的順序?qū)⑹仟氁粺o二的。另一方面,Unicode Consortium推薦組合形式來進(jìn)行存儲以實現(xiàn)與歷史遺留編碼轉(zhuǎn)換出來的字符串更為兼容。
兩種相等在做字符串比較的時候都很方便,尤其是在進(jìn)行排序和搜索的時候。但需要記住的是,盡量不要用compatibility equivalence來正交化某個會永久存儲的字符串。因為 it can alter the text’s meaning:

Normalization Forms KC and KD must not be blindly applied to arbitrary text. Because they erase many formatting distinctions, they will prevent round-trip conversion to and from many legacy character sets, and unless supplanted by formatting markup, they may remove distinctions that are important to the semantics of the text. It is best to think of these Normalization Forms as being like uppercase or lowercase mappings: useful in certain contexts for identifying core meanings, but also performing modifications to the text that may not always be appropriate.

Glyph變體
一些fonts為單個字符提供了多個shape變體(glyphs),而且Unicode提供了稱為 variation sequences 的機(jī)制以允許用戶選擇特定的變體。它的工作原理和combining字符序列挺像:一個由256個變體選擇子(VS1-VS256, U+FE00 to U+FE0F, U+E0100 to U+E01EF)所跟隨的基本字符。此標(biāo)準(zhǔn)還區(qū)分Standardized Variation Sequences(定義在Unicode標(biāo)準(zhǔn)中)和Ideographic Variation Sequences (由第三方提交到Unicode consortium并且一旦注冊可以被任何人使用),從技術(shù)層面來說,這兩者沒有什么區(qū)別。standardized variation sequences的例子是emoji的style,很多emoji都有?表情和正常字符兩種style,一個五顏六色的"emoji style"和一個黑白,更像符號的"text style"。比如 UMBRELLA WITH RAIN DROPS ?字符 (U+2614) 看起來可以是: ?? (U+2614 U+FE0F) 或者: ?? (U+2614 U+FE0E)

Unicode Transformation Formats

正如上面所看到的,將字符映射到code point僅僅只是第一步,還必須定義code point值在內(nèi)存及磁盤上的存儲形式。Unicode標(biāo)準(zhǔn)定義了幾種并命名為UTF,大家普遍稱之為編碼,但由于它使用Unicode編碼的字符并編碼在UTF中,所以沒必要區(qū)分這兩步。

UTF32

每個code point使用32位,雖然簡單,但空間使用太低效。

UTF16和Surrogate Pairs(代理編碼對)的概念

UTF16更普遍也與NSString的Unicode實現(xiàn)更相關(guān),它是用16位寬的術(shù)語稱為code units來定義的。UTF16本身是變長編碼,BMP中每個code point直接映射到一個code unit。由于BMP幾乎涵蓋了所有常用字符。其它plane中不常用的字符使用兩上code unit編碼。這種由兩個code unit一起表示一個code point的方法是Surrogate Pairs。
為避免UTF16編碼字符串中模棱兩可的字節(jié)序列,也為了使Surrogate Pairs的檢測更簡單,Unicode標(biāo)準(zhǔn)將U+D800 到 U+DFFF這個序列保留以供UTF16使用,這個序列中的code point永遠(yuǎn)不會分配字符。當(dāng)應(yīng)用在UTF16中看到這個range中的值時,它就知道它遭遇了Surrogate Pairs的一部分,實際的編碼算法是很簡單的, Wikipedia article for UTF-16可以看到更多。UTF16也是導(dǎo)致出現(xiàn)了看起來很奇怪的21位code point方案的原因,因為它最多能編碼的值是U+10FFFF。它的做法是將BMP之外的code point減去0x10000,再將剩余的高10位加上D800-DBFF,低10位加上DC00-DFFFF,組成兩個16位的序列。首先解決了通過21位Unicode字符的問題,其次,在解析UTF16時,根據(jù)每個code unit的值即可判斷它是不是應(yīng)當(dāng)解析為字符,還是需要將其與其后的unit組成Surrogate Pairs。比如字符U+10437的code point為0001 0000 0100 0011 0111,UTF16值為1101 1000 0000 0001 1101 1100 0011 0111,UTF16 code units為D801 DC37。
與所有多字節(jié)編碼范式相同的是,UTF16(和UTF32)也必須考慮字節(jié)順序。對于內(nèi)存中的字符串,自然而然地會采用CPU所采用的端實現(xiàn)。對于存儲在磁盤或者在網(wǎng)絡(luò)上傳輸,UTF16也允許在字符串的開頭插入Byte Order Mark (BOM)。BOM是一個code unit值為U+FEFF,通過檢查文件的開頭兩個字節(jié),解碼器即可識別它的字節(jié)序。BOM是可選的,而且標(biāo)準(zhǔn)將大端字節(jié)序作為默認(rèn)的選擇。字節(jié)序這一項復(fù)雜性是其并沒有廣泛用于文件存儲及網(wǎng)絡(luò)傳輸格式的原因,雖然OSX和windows都將其作為內(nèi)部使用。

UTF8

由于前256個Unicode code point與 通用的ISO-8859-1 (Latin 1) 編碼相同,UTF16仍然對于英語及西歐文本來說浪費了很多空間,因為高8位基本都是0。也許更致命的是,對于遺留的ASCII編碼的文本使用UTF16呈現(xiàn)起來是個嚴(yán)峻的挑戰(zhàn)。 UTF-8 由Ken Thompson (of Unix fame) 和 Rob Pike 開發(fā)并修復(fù)了這些不足,這個設(shè)計很棒,而且強(qiáng)烈推薦閱讀 Rob Pike’s account of how it was created
UTF8使用1到4個字節(jié)編碼一個code point。0到127的直接映射到一個字節(jié)(所以對于僅包含這些字符的文本,UTF8與ASCII編碼是一樣的)。接下來的1920個code point使用兩個字節(jié),其它所有的BMP中的code point需要3個字節(jié),其它unicode plane中的code point使用4個字節(jié)。由于UTF8基于8位的code units,它不需要關(guān)心字節(jié)序(雖然有些程序在UTF8文件中添加了多余的BOM)
空間效率和不需要關(guān)心字節(jié)序使UTF8成為了存儲和交換unicode文本的最佳編碼,它成為了事實上的文件格式,網(wǎng)絡(luò)協(xié)議和web apis。

NSString和Unicode

NSString是完全建立在Unicode上的,但apple對它的解釋是完全錯誤的,也即是 Apple’s documentation has to say about CFString
objects
中所說的

Conceptually, a CFString object represents an array of Unicode characters (UniChar
) along with a count of the number of characters. … The [Unicode] standard defines a universal, uniform encoding scheme that is 16 bits per character.

這是完全錯誤的,因為我們已經(jīng)知道Unicode編碼實際上是21位的,NSString的說明也同樣誤導(dǎo)

A string object presents itself as an array of Unicode characters …. You can determine how many characters a string object contains with the length
method and can retrieve a specific character with the characterAtIndex:
method. These two “primitive” methods provide basic access to a string object.

這份說明乍看起來好像是那么回事,但往深了去看,卻發(fā)現(xiàn)characterAtIndex:方法的返回值是unichar,是一個16位的無符號整數(shù),很顯然,并不足以代表21位的Unicode字符。

typedef unsigned short unichar;

真相是NSString實際上代表的是一個UTF16 code unit的數(shù)組。相應(yīng)地,length方法返回的是字符串中code unit數(shù)目。在NSString首次發(fā)布的時候是1994年,與Foundation Kit一起發(fā)布,那里Unicode仍是16位的編碼,更大范圍的Unicode和UTF16的surrogate character機(jī)制是在1996年Unicode 2.0中引入的。從今天的視角來看unichar和characterAtIndex:方法是很糟糕的,因為它會加深程序員對Unicode code point和UTF16 code units之間的誤解。codeUnitAtIndex:將會是一個比characterAtIndex:好得多得多的方法名。
如果關(guān)于NSString你只想記住一件事情,那么請記得NSString代表UTF16編碼的文本。NSString的長度,索引及ranges都是基于UTF16 code units?;谶@些概念的方法提供了不可靠的信息,除非你知道字符串的內(nèi)容且做了適當(dāng)?shù)念A(yù)防措施,無論何時文檔中提到字符和unichar,都指的是code units。Apple文檔在string programming guide中正確地描述了我們提到的概念但繼續(xù)使用了錯誤的概念來使用character。墻裂建議閱讀Characters and Grapheme Clusters,解釋了這是怎么回事。
雖然NSString概念上是基于UTF16的,但這并不意味著內(nèi)部總是使用UTF16的數(shù)據(jù)。它也沒有承諾過它的內(nèi)部實現(xiàn)。事實上,CFString總是在 存儲上嘗試變得更高效,基于字符串的內(nèi)容以及保持向UTF16 code units轉(zhuǎn)換O(1)的復(fù)雜度。可以讀下CFString source code

常見的陷阱

第一個使用Unicode字符序列創(chuàng)建字符串,默認(rèn)情況下Cland希望源文件是UTF8編碼的,只要確保Xcode使用UTF8存儲文件,則可以向其中插入任何的Character Viewer中的字符。如果你更喜歡code point,可以輸入@"\u266A"(?) 直到U+FFFF ?或者 @"\U0001F340"(??)等位于BMP之外的code point。有趣的是 C99并不允許把這些通用字符名用在標(biāo)準(zhǔn)C字符集中的字符上。所以下面這樣會失敗

NSString *s = @"\u0041";
// Latin capital letter A// error: character 'A' cannot be specified by a universal character name

我覺得對于創(chuàng)建字符串變量的場景應(yīng)該避免使用格式化標(biāo)識%C,它需要unichar,很容易會在code unit和code points中產(chǎn)生混淆,但對log輸出是有用的。

長度

-[NSString length]返回的是字符串中unichar的個數(shù),而基于我們已經(jīng)知道3個Unicode特性,這個值是可能與最終可見字符數(shù)不一樣的。
1 對于BMP之外的字符,比如emoji,原本一般只會遇到UTF16下一個code unit的BMP字符而很難遇到的surrogate pairs的情況,現(xiàn)在由于emoji的出現(xiàn)不得不考慮并恰當(dāng)處理了。

NSString *s = @"\U0001F30D";
// earth globe emoji ??NSLog(@"The length of %@ is %lu", s, [s length]);
// => The length of ?? is 2

這個問題的解決方案是一個小hack,只需要簡單地計算使用UTF32編碼字符串需要的字節(jié)數(shù)然后除以4就可以了

NSUInteger realLength =
[s lengthOfBytesUsingEncoding:NSUTF32StringEncoding] / 4;
NSLog(@"The real length of %@ is %lu", s, realLength);
// => The real length of ?? is 1

2 combining字符序列:如果é編碼為分解的形式,它是會當(dāng)成兩個code unit的

NSString *s = @"e\u0301";
// e + ′NSLog(@"The length of %@ is %lu", s, [s length]);
// => The length of é is 2

結(jié)果2在某種程度上是正確的,是因為字符串真的包含兩個Unicode字符,但卻與人眼觀察到的可見長度不一樣。可以使用方法precomposedStringWithCanonicalMapping將字符串正交化并得到正交化形式的C (precomposed characters)來獲取更好的結(jié)果:

NSString *n = [s precomposedStringWithCanonicalMapping];
NSLog(@"The length of %@ is %lu", n, [n length]);
// => The length of é is 1

但可惜的是這并不能應(yīng)對所有的情況,因為?只有大多數(shù)常見的combining字符序列于precomposed form 可用,基礎(chǔ)字符和其它combining marks的結(jié)合仍會是和decomposed form一樣,就算是正交化之后。如果真想知道肉眼可見長度,則需要自己手動遍歷字符串并計算??蓞㈤喯旅娴腖ooping部分
1.Variation sequences: These behave like decomposed combining character sequences, so the variation selector counts as a separate character.(這段文字還不知道為什么會出現(xiàn)在原文中)

隨機(jī)訪問

直接通過characterAtIndex:獲取的索引訪問unichar有同樣的問題,因為字符串可能包含combining字符序列,surrogate pairs或者Variation sequences。蘋果用composed character sequence這個術(shù)語指代所有這些特征。這個術(shù)語很容易混淆,別把蘋果的術(shù)語composed character sequences 與Unicode術(shù)語combining character sequences混淆了,后者只是前者的一部分。用rangeOfComposedCharacterSequenceAtIndex:
方法找出給定位置的unichar是否是代表單個字符(當(dāng)然它可以包含多個code points)的code units序列的一部分。任何時候需要將字符串的range中的未知內(nèi)容傳遞給另一個方法的時候,為確保Unicode字符沒有被撕開,都需要做這個操作。

Looping

通過rangeOfComposedCharacterSequenceAtIndex:方法,可以正確地遍歷字符串中所有字符,但每次需要遍歷字符串時都這樣做會很不方便。幸運的是NSString提供了enumerateSubstringsInRange:options:usingBlock:
方法,這個方法幫你隱藏起了Unicode的特性,提供了遍歷字符串中字符序列,詞,行,句子及段落的便捷方法。甚至可以通過傳入NSStringEnumerationLocalized選項,將用戶當(dāng)前的locale也作為判定斷詞和斷句的依據(jù)之一。遍歷字符可以使用NSStringEnumerationByComposedCharacterSequences選項:

NSString *s = @"The weather on \U0001F30D is \U0001F31E today.";
// The weather on ?? is ?? today.NSRange fullRange = NSMakeRange(0, [s length]);
[s enumerateSubstringsInRange:fullRange
options:NSStringEnumerationByComposedCharacterSequences usingBlock:
^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop)
{ NSLog(@"%@ %@", substring, NSStringFromRange(substringRange));}
];

從這個棒到乖乖的方法也可以看出Apple希望我們將字符串當(dāng)作子字符串的集合來對待,而不是當(dāng)做字符的集合。因為一個unichar并不足以表示一個Unicode字符,而且有些字符是由多個Unicode code point組成的。這個方法是相比來說最近才添加的(in OS X 10.6 and iOS 4.0),之前遍歷字符串中的字符可沒這么方便

比較

字符串對象本身是并非正交的,除非手動轉(zhuǎn)換,這也意味著比較的字符串中如果帶有combining字符序列的字符,比較結(jié)果將會出錯。因為 isEqual:
isEqualToString:
都是一個字節(jié)一個字節(jié)地比較字符串的。如果想用?組合的和分解的字符串變體用于比較,必須將字符串先正交化:

NSString *s = @"\u00E9"; // é
NSString *t = @"e\u0301"; // e + ′
BOOL isEqual = [s isEqualToString:t];
NSLog(@"%@ is %@ to %@", s, isEqual ? @"equal" : @"not equal", t);
// => é is not equal to é
// Normalizing to form C
NSString *sNorm = [s precomposedStringWithCanonicalMapping];
NSString *tNorm = [t precomposedStringWithCanonicalMapping];
BOOL isEqualNorm = [sNorm isEqualToString:tNorm];
NSLog(@"%@ is %@ to %@", sNorm, isEqualNorm ? @"equal" : @"not equal", tNorm);
// => é is equal to é

當(dāng)然還有其它選擇,那就是用 compare:
方法或者像localizedCompare:
這樣compare:的變體方法,會返回使用compatibility equivalent版本字符串的匹配結(jié)果。但這一點Apple并沒有仔細(xì)注明,但需要注意的是通常大家在使用過程中希望的匹配是canonical equivalence,但compare:方法并沒有給這個選項

NSString *s = @"ff";
// ffNSString *t = @"\uFB00";
// ? ligatureNSComparisonResult result = [s localizedCompare:t];
NSLog(@"%@ is %@ to %@", s, result == NSOrderedSame ? @"equal" : @"not equal", t);
// => ff is equal to ?

如果你需要用compare:方法但不想考慮equivalence,compare:options:
這個方法可以指定NSLiteralSearch,也可以加速匹配,就是說連compatibility equivalent相等的也可能會判定為不相等。

從文件或者網(wǎng)絡(luò)讀取文本

一般來講,只有在你知道一段文本的編碼的時候這段文本數(shù)據(jù)才有用,而當(dāng)你從網(wǎng)絡(luò)上下載了一段文本數(shù)據(jù)的時候,你通常是已經(jīng)事先知道了它的編碼,或者可以直接從HTTP中獲取到。隨后用 -[NSString initWithData:encoding:]
方法就可以水到渠成地完成NSString的創(chuàng)建了。
然而文本文件本身并沒有把它的編碼寫在文本文件中,但NSString通??梢酝ㄟ^查看文件的擴(kuò)展屬性或者使用啟發(fā)式邏輯(heuristics)(比如特定的二進(jìn)制序列絕對不會出現(xiàn)在有效的UTF8文件中)來獲取文件的編碼方式。為用已知的編碼格式從文件中讀取文本,使用[NSString initWithContentsOfURL:encoding:error:]
。而讀取未知編碼格式的文件,Apple提供了這個指引

如果強(qiáng)制去猜測編碼方式:

  1. 試著使用stringWithContentsOfFile:usedEncoding:error:
    或者initWithContentsOfFile:usedEncoding:error:
    (或者基于URL的等效方法),這些方法會試著判定資源的編碼方式,如果成功會返回編碼方式的引用
  2. 如果第1步失敗,試著指定UTF8作為編碼方式讀取資源
  3. 如果第2步失敗,嘗試一種合適的遺留編碼。
    這里的合適指的是依賴于當(dāng)前所使用的環(huán)境,?可能是默認(rèn)的 C 字符串編碼, 可能是 ISO 或者 Windows Latin 1, ?或者其它?,取決于數(shù)據(jù)來源于哪里
  4. 最終可以嘗試使用NSAttributedString的加載方法,比如initWithURL:options:documentAttributes:error:等。
    這些方法嘗試嘗試加載純文本文件并返回使用的編碼,如果你的應(yīng)用對文本沒有特別的需求的話,?這些方法可以用在某種程度上來說任意的文本文檔上。但它們可能對于系統(tǒng)級工具或者非自然語言的文本并不合適。

寫文本到文件中

之前已經(jīng)提到,除非有特別的說明,否則純文本文件的編碼,網(wǎng)絡(luò)傳輸或者你自己的文件格式或者網(wǎng)絡(luò)協(xié)議都應(yīng)該使用UTF8。寫字符串到文件中用writeToURL:?atomically:?encoding:?error:
。
這個方法會自動為UTF16或者UTF32編碼的文件添加字節(jié)序的標(biāo)記。它也會在文件擴(kuò)展信息中使用com.apple.TextEncoding屬性名添加文件編碼信息。由于initWithContentsOf…:usedEncoding:error:方法顯然知道這個屬性,使用標(biāo)準(zhǔn)的NSString方法可以確保你在從文件中加載文本的時候使用正確的編碼。

結(jié)語

文本是很復(fù)雜的,雖然Unicode已經(jīng)大大方便了處理文本的過程,但并不能免除程序員了解它的工作原理的工作量。當(dāng)今的很多app實際都需要處理多門語言的文本。即使你的app并不是localized到中文或者阿拉伯文,只要你支持任何文本的輸入,都必須準(zhǔn)備處理所有的Unicode字符。
你需要用當(dāng)前世界的所有語言輸入來測試你的app,并確保在單元測試中使用盡可能多的emoji和非拉丁文本,如果你不能輕松地獲取特定各類的文本,可以上Wikipedia在 Wikipedia of your choice中從任意文章中復(fù)制單詞即可。

擴(kuò)展閱讀

Joel Spolsky: The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets. This is more than 10 years old and not specific to Cocoa, but still a very good overview.
Ross Carter gave a wonderful talk at NSConference 2012 titled You too can speak Unicode. It’s a very entertaining talk and I highly recommend watching it. I based part of this article on Ross’s presentation. Scotty from NSConference was kind enough to make the video available to all objc.io readers. Thanks!
The Wikipedia article on Unicode is great.
unicode.org, the website of the Unicode Consortium, not only has the full standard and code chart references, but also a wealth of other interesting information. The extensive FAQ section is excellent.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容