i 9 NSString和Unicode

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

Unicode的前世今生

眾所周知,計(jì)算機(jī)是無法直接處理文本(即一串字符)的辜王,它們只能處理數(shù)字劈狐。所以計(jì)算機(jī)中是使用一串?dāng)?shù)字來表示文本的,而實(shí)現(xiàn)數(shù)字到文本的映射即稱之為編碼呐馆。
最為人熟知的編碼即是ASCII碼肥缔,它是一個(gè)7位的編碼,將英文字母汹来,數(shù)字续膳,標(biāo)點(diǎn)符號(hào)和控制字符映射到0到127之間的整數(shù)。隨后出現(xiàn)了很多不同的8位編碼以將除了英文之外的字符使用到計(jì)算機(jī)中俗慈,它們大多基于ASCII姑宽,即使用了未使用的第8位以編碼其它的字母遣耍,符號(hào)或者整個(gè)字母表(西里爾字符和希臘字符)
這些編碼互相之間是不兼容的闺阱,而且8位空間仍然不足以承載即使是歐洲所使用的所有字符,更別說世界范圍內(nèi)其它的字符了舵变。當(dāng)時(shí)基于文本的計(jì)算機(jī)系統(tǒng)的一個(gè)問題是同一時(shí)刻只能使用一種編碼(也稱為code page)酣溃。如果在一臺(tái)機(jī)器上寫文本,在另一臺(tái)使用不同編碼頁的機(jī)器上打開時(shí)纪隙,所有128到255對(duì)應(yīng)的字符都會(huì)解析錯(cuò)誤赊豌。
東亞的字符比如中文,日文绵咱,朝鮮文還有另一個(gè)問題碘饼,那就是CJK所包含的字符如此之多,8位根本不夠編碼悲伶,所以催生了16位字符編碼的出現(xiàn)艾恼。而一旦在處理不止一個(gè)字節(jié)能滿足的值的時(shí)候,如何將它們存儲(chǔ)在內(nèi)存和磁盤上就不是一個(gè)小問題了麸锉。你需要根據(jù)字節(jié)序定義第二種映射規(guī)則钠绍,或者使用變長編碼代替等寬編碼。需要注意的是這第二種映射其實(shí)也是一種形式的編碼花沉,而我們用了同一個(gè)詞“編碼”其實(shí)是使大家對(duì)編碼這一概念產(chǎn)生混淆的基本來源柳爽,下面講UTF8和UTF16的時(shí)候會(huì)再進(jìn)行討論媳握。
現(xiàn)代操作系統(tǒng)大多都已經(jīng)擺脫同一時(shí)刻只能使用一個(gè)code page的束縛了,所以只要每個(gè)文檔正確地報(bào)告它的編碼磷脯,處理數(shù)百種不同的編碼都是可能的蛾找,不可能的是在同一個(gè)文檔中使用多種編碼,即在同一文檔中用多種語言編寫赵誓,而也就是這個(gè)原因?yàn)榍癠nicode時(shí)代的棺木訂上了最后一個(gè)釘子腋粥。
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)中所有字符和符號(hào)都定義了唯一的數(shù)值展辞,每個(gè)這樣的數(shù)據(jù)都稱為code point颂斜,表示為U+xxxx缭受,xxxx為4到6個(gè)16進(jìn)制數(shù)爵憎。比如code point U+0041在拉丁字母表中代表A霜定,而U+1F61B代表名為FACE WITH STUCK-OUT TONGUE的emoji ??辖源,需要注意的是字符的名字也是Unicode標(biāo)準(zhǔn)的一部分勋锤《环可以用標(biāo)準(zhǔn)代碼表或者使用OS X上的

charater viewer
即圖中的顯示字符顯示程序查找code point伊履,在我10.10.5的系統(tǒng)上核无,未能在選中某個(gè)字符比如表情之后在右邊顯示其Unicode編碼扣唱,可以通過右擊表情“拷貝字符簡介”并找地方粘貼出來以顯示其編碼。
如上述提到的其它編碼一樣团南,Unicode只是抽象地描述了字符噪沙,定義了字符對(duì)應(yīng)的值和名字等信息,但并沒有規(guī)定它顯示的glyph是怎樣吐根。比如對(duì)于中日朝文中都使用的某個(gè)漢字正歼,Unicode定義了相同的code point但在這三種語言印刷系統(tǒng)中,這個(gè)漢字的顯示glyph并不一定是相同的拷橘,因?yàn)樗鼈兪窃诟髯試业挠∷⑾到y(tǒng)中各自定義的局义。
Unicode最初是16位的編碼,可定義65536個(gè)字符冗疮,被認(rèn)為可以容納下世界范圍內(nèi)當(dāng)今的所有字符萄唇,廢棄及很少使用的字符被認(rèn)為是需要放到Private use Areas的,即65536中的某一段保留區(qū)域U+E000–U+F8FF(當(dāng)然赌厅,由于Unicode其實(shí)是一個(gè)21位的編碼穷绵,所以還有BMP之外的兩段private use area,即plane15和16中的U+F0000–U+FFFFD, U+100000–U+10FFFD)特愿,用戶可以將這段區(qū)域的code point映射到自己定義的字符中仲墨,所以對(duì)于不同的定義勾缭,code point是會(huì)沖突的。Apple在這段區(qū)域中定義了一些符號(hào)和控制字符目养,雖然大多數(shù)都已經(jīng)不推薦使用俩由,一個(gè)著名的例外是apple的logo字符U+F8FF: ?,如果你此刻并不是使用apple平臺(tái)的機(jī)器癌蚁,則這個(gè)字符的顯示就會(huì)根據(jù)你機(jī)器的平臺(tái)而顯示一個(gè)完全不同的字符幻梯。
Unicode編碼空間隨后就擴(kuò)展成了21位,以容納歷史上使用或者很少使用的?日本漢字或者中文字符努释。這是一個(gè)很重要的點(diǎn)碘梢,而且之后即將介紹的NSString也與它很相關(guān),那就是Unicode并不是16位編碼伐蒂。21位編碼提供了1,114,112 code point煞躬。目前只使用了10%左右的空間,所以還有大把的code point可用逸邦。
Unicode編碼空間劃分為17個(gè)65536空間恩沛,每個(gè)65536空間稱為一個(gè)plane,Plane 0稱為Basic Multilingual Plane (BMP)缕减,也是目前所使用的絕大多數(shù)字符所在的Plane雷客,除了著名的emoji,其它的plane稱為增補(bǔ)planes桥狡,且大部分都是空的搅裙。

特有的Unicode特征

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

結(jié)合字符序列

為了與之前的編碼兼容,有些字符不僅可以看成是單個(gè)code point局雄,也可以看成是多個(gè)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中的變體,有這兩種存在的原因其實(shí)是有些Unicode實(shí)現(xiàn)對(duì)precomposed字符glyph的處理好穆桂,而對(duì)分解形式的glyph可能無法辨認(rèn)所造成宫盔,又或者有的Unicode實(shí)現(xiàn)相反,這種情況所造成的享完。結(jié)合字符不僅在西方出現(xiàn)在灼芭,在東方比如Hangul,音節(jié)?可以由單個(gè)code point(U+AC00) 表示,也可以由? + ? (U+1100 U+1161)序列表示般又。
在Unicode用語中彼绷,這兩種形式并不是等價(jià)的巍佑,因?yàn)榘煌腸ode point,卻是canonically equivalent:即它們有相同的形態(tài)(按道理是看起來應(yīng)該是一樣的寄悯,但實(shí)際是不是一樣就得問上帝了)和意思萤衰。

重復(fù)字符

許多看起來相同的字符使用不同的code point編碼了多次,且代表了不同的意思猜旬。比如拉丁字母A在形狀上與西里爾字母A (U+0410)一樣脆栋,但它們其實(shí)是不同的。將它們當(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)熟嫩,因?yàn)?ngstr?m實(shí)際是瑞典語的大寫字母丘薛,它們是完全相同的,在Unicode中它們不相等但是canonically相等邦危。
很多字符和序列則在廣義上是重復(fù)的洋侨,在Unicode標(biāo)準(zhǔn)中稱為 compatibility equivalence ,兼容序列代表相同的抽象字符倦蚪,但可能沒有相同的視覺體現(xiàn)或者行為希坚。比如很多希臘字母,同時(shí)也用于數(shù)學(xué)了技術(shù)符號(hào)陵且。羅馬數(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中字符串相等并不是一個(gè)簡單的概念获洲,除了比較兩個(gè)字符串,一個(gè)個(gè)code point的比較殿如,還要比較canonical equivalence 和compatibility equivalence贡珊。Unicode為此提供了為所有相等的序列生成一個(gè)獨(dú)一無二的code point序列的標(biāo)準(zhǔn)正交算法 。相等的標(biāo)準(zhǔn)既可以是canonical (NF) ?或者 compatibility (NFK)涉馁,四種Unicode的正交形式及獲取此種正交形式的算法如下

Unicode正交算法.png

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

出于比較字符串的目的门岔,既可以將它們?nèi)空换癁榉纸猓―)形式,也可以正交化為?組合(C)形式烤送。通常分解形式更快寒随,因?yàn)榻M合形式的算法包含兩個(gè)步驟:字符會(huì)先分解然后重新進(jìn)行組合。如果一個(gè)字符序列包含多個(gè)combining marks,combining marks的順序在分解之后的順序?qū)⑹仟?dú)一無二的妻往。另一方面互艾,Unicode Consortium推薦組合形式來進(jìn)行存儲(chǔ)以實(shí)現(xiàn)與歷史遺留編碼轉(zhuǎn)換出來的字符串更為兼容。
兩種相等在做字符串比較的時(shí)候都很方便蒲讯,尤其是在進(jìn)行排序和搜索的時(shí)候忘朝。但需要記住的是,盡量不要用compatibility equivalence來正交化某個(gè)會(huì)永久存儲(chǔ)的字符串判帮。因?yàn)?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為單個(gè)字符提供了多個(gè)shape變體(glyphs)局嘁,而且Unicode提供了稱為 variation sequences 的機(jī)制以允許用戶選擇特定的變體。它的工作原理和combining字符序列挺像:一個(gè)由256個(gè)變體選擇子(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并且一旦注冊(cè)可以被任何人使用)悦昵,從技術(shù)層面來說,這兩者沒有什么區(qū)別晌畅。standardized variation sequences的例子是emoji的style但指,很多emoji都有?表情和正常字符兩種style,一個(gè)五顏六色的"emoji style"和一個(gè)黑白抗楔,更像符號(hào)的"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)存及磁盤上的存儲(chǔ)形式剩岳。Unicode標(biāo)準(zhǔn)定義了幾種并命名為UTF,大家普遍稱之為編碼入热,但由于它使用Unicode編碼的字符并編碼在UTF中拍棕,所以沒必要區(qū)分這兩步。

UTF32

每個(gè)code point使用32位勺良,雖然簡單绰播,但空間使用太低效。

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

UTF16更普遍也與NSString的Unicode實(shí)現(xiàn)更相關(guān)尚困,它是用16位寬的術(shù)語稱為code units來定義的蠢箩。UTF16本身是變長編碼,BMP中每個(gè)code point直接映射到一個(gè)code unit尾组。由于BMP幾乎涵蓋了所有常用字符忙芒。其它plane中不常用的字符使用兩上code unit編碼。這種由兩個(gè)code unit一起表示一個(gè)code point的方法是Surrogate Pairs讳侨。
為避免UTF16編碼字符串中模棱兩可的字節(jié)序列,也為了使Surrogate Pairs的檢測(cè)更簡單奏属,Unicode標(biāo)準(zhǔn)將U+D800 到 U+DFFF這個(gè)序列保留以供UTF16使用跨跨,這個(gè)序列中的code point永遠(yuǎn)不會(huì)分配字符。當(dāng)應(yīng)用在UTF16中看到這個(gè)range中的值時(shí)勇婴,它就知道它遭遇了Surrogate Pairs的一部分拘悦,實(shí)際的編碼算法是很簡單的添诉, Wikipedia article for UTF-16可以看到更多。UTF16也是導(dǎo)致出現(xiàn)了看起來很奇怪的21位code point方案的原因乐横,因?yàn)樗疃嗄芫幋a的值是U+10FFFF条霜。它的做法是將BMP之外的code point減去0x10000搀矫,再將剩余的高10位加上D800-DBFF,低10位加上DC00-DFFFF刻肄,組成兩個(gè)16位的序列瓤球。首先解決了通過21位Unicode字符的問題,其次敏弃,在解析UTF16時(shí)卦羡,根據(jù)每個(gè)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é)順序隅要。對(duì)于內(nèi)存中的字符串蝴罪,自然而然地會(huì)采用CPU所采用的端實(shí)現(xiàn)。對(duì)于存儲(chǔ)在磁盤或者在網(wǎng)絡(luò)上傳輸步清,UTF16也允許在字符串的開頭插入Byte Order Mark (BOM)要门。BOM是一個(gè)code unit值為U+FEFF,通過檢查文件的開頭兩個(gè)字節(jié)廓啊,解碼器即可識(shí)別它的字節(jié)序欢搜。BOM是可選的,而且標(biāo)準(zhǔn)將大端字節(jié)序作為默認(rèn)的選擇谴轮。字節(jié)序這一項(xiàng)復(fù)雜性是其并沒有廣泛用于文件存儲(chǔ)及網(wǎng)絡(luò)傳輸格式的原因炒瘟,雖然OSX和windows都將其作為內(nèi)部使用。

UTF8

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

NSString和Unicode

NSString是完全建立在Unicode上的省骂,但apple對(duì)它的解釋是完全錯(cuò)誤的蟀淮,也即是 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.

這是完全錯(cuò)誤的最住,因?yàn)槲覀円呀?jīng)知道Unicode編碼實(shí)際上是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,是一個(gè)16位的無符號(hào)整數(shù)策治,很顯然脓魏,并不足以代表21位的Unicode字符。

typedef unsigned short unichar;

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

常見的陷阱

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

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

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

長度

-[NSString length]返回的是字符串中unichar的個(gè)數(shù)鳍徽,而基于我們已經(jīng)知道3個(gè)Unicode特性资锰,這個(gè)值是可能與最終可見字符數(shù)不一樣的。
1 對(duì)于BMP之外的字符阶祭,比如emoji绷杜,原本一般只會(huì)遇到UTF16下一個(gè)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

這個(gè)問題的解決方案是一個(gè)小hack接剩,只需要簡單地計(jì)算使用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字符序列:如果é編碼為分解的形式,它是會(huì)當(dāng)成兩個(gè)code unit的

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

結(jié)果2在某種程度上是正確的萨咳,是因?yàn)樽址娴陌瑑蓚€(gè)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)對(duì)所有的情況鹃两,因?yàn)?只有大多數(shù)常見的combining字符序列于precomposed form 可用,基礎(chǔ)字符和其它c(diǎn)ombining marks的結(jié)合仍會(huì)是和decomposed form一樣舀凛,就算是正交化之后俊扳。如果真想知道肉眼可見長度,則需要自己手動(dòng)遍歷字符串并計(jì)算猛遍〔黾牵可參閱下面的Looping部分
1.Variation sequences: These behave like decomposed combining character sequences, so the variation selector counts as a separate character.(這段文字還不知道為什么會(huì)出現(xiàn)在原文中)

隨機(jī)訪問

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

Looping

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

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));}
];

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

比較

字符串對(duì)象本身是并非正交的,除非手動(dòng)轉(zhuǎn)換阁吝,這也意味著比較的字符串中如果帶有combining字符序列的字符砚婆,比較結(jié)果將會(huì)出錯(cuò)。因?yàn)?isEqual:
isEqualToString:
都是一個(gè)字節(jié)一個(gè)字節(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:的變體方法甲馋,會(huì)返回使用compatibility equivalent版本字符串的匹配結(jié)果埂奈。但這一點(diǎn)Apple并沒有仔細(xì)注明,但需要注意的是通常大家在使用過程中希望的匹配是canonical equivalence定躏,但compare:方法并沒有給這個(gè)選項(xiàng)

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:
這個(gè)方法可以指定NSLiteralSearch,也可以加速匹配痊远,就是說連compatibility equivalent相等的也可能會(huì)判定為不相等垮抗。

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

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

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

  1. 試著使用stringWithContentsOfFile:usedEncoding:error:
    或者initWithContentsOfFile:usedEncoding:error:
    (或者基于URL的等效方法)班缎,這些方法會(huì)試著判定資源的編碼方式,如果成功會(huì)返回編碼方式的引用
  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)用對(duì)文本沒有特別的需求的話,?這些方法可以用在某種程度上來說任意的文本文檔上罢屈。但它們可能對(duì)于系統(tǒng)級(jí)工具或者非自然語言的文本并不合適嘀韧。

寫文本到文件中

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

結(jié)語

文本是很復(fù)雜的,雖然Unicode已經(jīng)大大方便了處理文本的過程绩衷,但并不能免除程序員了解它的工作原理的工作量蹦魔。當(dāng)今的很多app實(shí)際都需要處理多門語言的文本。即使你的app并不是localized到中文或者阿拉伯文咳燕,只要你支持任何文本的輸入勿决,都必須準(zhǔn)備處理所有的Unicode字符。
你需要用當(dāng)前世界的所有語言輸入來測(cè)試你的app招盲,并確保在單元測(cè)試中使用盡可能多的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)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末咆繁,一起剝皮案震驚了整個(gè)濱河市讳推,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌玩般,老刑警劉巖银觅,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異坏为,居然都是意外死亡究驴,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門匀伏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來洒忧,“玉大人,你說我怎么就攤上這事帘撰∨苣剑” “怎么了?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵摧找,是天一觀的道長。 經(jīng)常有香客問我牢硅,道長蹬耘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任减余,我火速辦了婚禮综苔,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘位岔。我一直安慰自己如筛,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布抒抬。 她就那樣靜靜地躺著杨刨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪擦剑。 梳的紋絲不亂的頭發(fā)上妖胀,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音惠勒,去河邊找鬼赚抡。 笑死,一個(gè)胖子當(dāng)著我的面吹牛纠屋,可吹牛的內(nèi)容都是我干的涂臣。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼售担,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼赁遗!你這毒婦竟也來了署辉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤吼和,失蹤者是張志新(化名)和其女友劉穎涨薪,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體炫乓,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡刚夺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了末捣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片侠姑。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖箩做,靈堂內(nèi)的尸體忽然破棺而出莽红,到底是詐尸還是另有隱情,我是刑警寧澤邦邦,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布安吁,位于F島的核電站,受9級(jí)特大地震影響燃辖,放射性物質(zhì)發(fā)生泄漏鬼店。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一黔龟、第九天 我趴在偏房一處隱蔽的房頂上張望妇智。 院中可真熱鬧,春花似錦氏身、人聲如沸巍棱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽航徙。三九已至,卻和暖如春豁状,著一層夾襖步出監(jiān)牢的瞬間捉偏,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國打工泻红, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留夭禽,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓谊路,卻偏偏與公主長得像讹躯,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • Swift學(xué)習(xí)有問必答群 : 313838956 ( mac版QQ有權(quán)限要求, 入群只能通過手機(jī)版 QQ申請(qǐng)...
    Guards翻譯組閱讀 6,605評(píng)論 9 13
  • 轉(zhuǎn)載自O(shè)bjeC中國 歷史 計(jì)算機(jī)沒法直接處理文本潮梯,它只和數(shù)字打交道骗灶。為了在計(jì)算機(jī)里用數(shù)字表示文本,我們指定了一個(gè)...
    玉米包谷閱讀 1,181評(píng)論 0 4
  • 你 我心心念念的你 卻不愿將你想起 太過美好的回憶 都不該輕易觸及 怕它褪去顏色 沒有昔日絢麗 你 我心心念念的你...
    秋未完閱讀 357評(píng)論 5 11
  • 或許每個(gè)人都有欲望占據(jù)上風(fēng)的時(shí)候秉馏。有時(shí)看百度云群里的那些視頻耙旦,卻發(fā)現(xiàn)很很奇怪的現(xiàn)象:很多視頻中都只是女生露臉而男生...
    風(fēng)思云起閱讀 166評(píng)論 0 0
  • 不知道從什么時(shí)候開始帆竹, “好人”一詞已經(jīng)脫離了它原有的意義绕娘, 而進(jìn)化成男女戀愛時(shí)的專用術(shù)語, 成為他們拒絕對(duì)方的一...
    澤小Ze閱讀 599評(píng)論 0 0