iOS開發(fā)Unicode和NSString

NSString 和 Unicode

NSString 是完全建立在 Unicode 之上的本刽。但是岛宦,這方面蘋果解釋得并不好。這是蘋果的文檔對(duì) CFString對(duì)象的說明CFString 也包含了 NSString 的底層實(shí)現(xiàn)):

從概念上來講,CFString 代表了一個(gè) Unicode 字符組成的數(shù)組和一個(gè)字符總數(shù)的計(jì)數(shù)⌒……[Unicode] 標(biāo)準(zhǔn)定義了一個(gè)通用、統(tǒng)一的編碼方案亩冬,其中每個(gè)字符 16 位艘希。

強(qiáng)調(diào)是我(原文作者)加的。這完全是錯(cuò)誤的硅急!我們已經(jīng)了解了 Unicode 是一種 21 位的編碼方案覆享。但是有了這樣的文檔,難怪很多人都認(rèn)為它是 16 位的呢铜秆。

NSString 的文檔同樣誤導(dǎo)人:

一個(gè)字符串對(duì)象代表著一個(gè) Unicode 字符組成的數(shù)組…… 可以用 length 方法來獲得一個(gè)字符串對(duì)象所包含的字符數(shù);用 characterAtIndex: 方法取得特定的字符讶迁。這兩個(gè)簡(jiǎn)單的方法為訪問字符串對(duì)象提供了基本的途徑连茧。

這段話初讀起來似乎好一些了,它沒有又扯淡地講 Unicode 字符是 16 位的巍糯。但深究后就會(huì)發(fā)現(xiàn)啸驯,characterAtIndex: 這個(gè)方法的返回值 unichar 不過是個(gè) 16 位的無符號(hào)整型罷了。顯然祟峦,它不夠用來表示 21 位的 Unicode 字符:

typedef unsigned short unichar;

事實(shí)是這樣的罚斗,NSString 對(duì)象代表的其實(shí)是用 UTF-16 編碼的碼元組成的數(shù)組。相應(yīng)地宅楞,length 方法的返回值也是字符串包含的碼元個(gè)數(shù)(而不是字符個(gè)數(shù))针姿。NSString 還在開發(fā)的時(shí)候(它最初是作為 Foundation Kit 的一部分在 1994 年發(fā)布的),Unicode 還是 16 位的厌衙;更廣的范圍和 UTF-16 的代理字符機(jī)制則是于 1996 年隨著 Unicode 2.0 引入的距淫。從現(xiàn)在的角度來看,unichar 這個(gè)類型和 characterAtIndex:這個(gè)方法的命名都很糟糕婶希,因?yàn)樗鼈兪钩绦騿T對(duì)于 Unicode 字符(碼點(diǎn))和 UTF-16 碼元兩個(gè)概念困惑的情況更加嚴(yán)重榕暇。如果像 codeUnitAtIndex: 這樣來命名則要好得多。

關(guān)于 NSString喻杈,最需要記住的是:NSString 代表的是用 UTF-16 編碼的文本彤枢,長(zhǎng)度、索引和范圍都基于 UTF-16 的碼元筒饰。除非你知道字符串的內(nèi)容缴啡,或者你提前有所防范,不然 NSString 類里的方法都是基于上述概念的瓷们,無法給你提供可靠的信息盟猖。每當(dāng)文檔提及「字符」(character)或者 unichar 時(shí)讨衣,它其實(shí)都說的是碼元。事實(shí)上式镐,在 String Programming Guide 里之后一個(gè)章節(jié)中反镇,文檔的表述是正確的,但繼續(xù)錯(cuò)誤地使用「字符」(character)這個(gè)詞娘汞。強(qiáng)烈建議你閱讀 Characters and Grapheme Clusters 這一章歹茶,里面很好地解釋了真實(shí)的情況。

請(qǐng)注意你弦,盡管在概念上 NSString 是基于 UTF-16 的惊豺,但這并不意味著這個(gè)類總是能與 UTF-16 編碼的數(shù)據(jù)很好地工作。它不保證內(nèi)部的實(shí)現(xiàn)(你可以子類化 NSString 來寫你自己的實(shí)現(xiàn))禽作。事實(shí)上尸昧,在保證快速的(時(shí)間復(fù)雜度 O(1) 級(jí)別)與 UTF-16 碼元轉(zhuǎn)換的同時(shí),CFString 盡可能有效率地利用內(nèi)存旷偿,這取決于字符串的內(nèi)容烹俗。你可以閱讀 CFString 的源代碼來自己驗(yàn)證。

常見的陷阱

了解了 NSString 和 Unicode萍程,你現(xiàn)在應(yīng)該能辨別出哪些操作對(duì)字符串有潛在的危險(xiǎn)幢妄。我們來看看這些操作,以及如何避免出現(xiàn)問題茫负。但首先蕉鸳,我們得知道怎么用任意的 Unicode 字符序列創(chuàng)建字符串。

默認(rèn)情況下忍法,Clang 會(huì)把源文件看作以 UTF-8 編碼的潮尝。只要你確保 Xcode 以 UTF-8 編碼保存文件,你就可以直接用字符顯示程序插入任意字符饿序。如果你更喜歡用碼點(diǎn)衍锚,最大到 U+FFFF 這個(gè)范圍內(nèi)的碼點(diǎn)你可以以 @"\u266A"(?)的方式輸入,BMP 外其它平面的碼點(diǎn)則以 @"\U0001F340"(??)的方式輸入嗤堰。有意思的是戴质,C99 不允許標(biāo)準(zhǔn) C 字符集里的字符用通用字符名(universal character name)來指定,因此不能這樣寫:

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

我認(rèn)為應(yīng)該避免使用格式化占位符 %C(使用 unichar 類型)來創(chuàng)建字符串變量踢匣,因?yàn)檫@樣很容易混淆碼元和碼點(diǎn)告匠。但是在輸出 log 信息時(shí) %C 很有用。

長(zhǎng)度

-[NSString length] 返回字符串里 unichar 的個(gè)數(shù)离唬。我們已經(jīng)了解了三個(gè)可能導(dǎo)致這個(gè)返回值與實(shí)際(可見)字符數(shù)不符的 Unicode 特性后专。

  1. 基本多文種平面外的字符:記住,BMP 里所有的字符在 UTF-16 里都可以用一個(gè)碼元表示输莺。所有其余的字符都需要兩個(gè)碼元(一個(gè)代理對(duì))戚哎÷惴蹋基本上所有現(xiàn)代使用的字符都在 BMP 里,因此在實(shí)際中很難遇到代理對(duì)型凳。然而丈冬,幾年前隨著 emoji 被引入 Unicode(在 1 號(hào)平面),這種情況已經(jīng)有所變化甘畅。emoji 已經(jīng)變得十分普遍埂蕊,你的代碼必須能夠正確處理它們:

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

    可以用一個(gè)小花招解決這個(gè)問題,直接計(jì)算字符串在 UTF-32 編碼下所需要的字節(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. 組合字符序列:如果字母 é 是以分解形式(e + ′)編碼的蓄氧,算作兩個(gè)碼元:

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

    這個(gè)字符串包含了兩個(gè) Unicode 字符,在這個(gè)意義上槐脏,返回值 2 是正確的喉童,但顯然正常人都不會(huì)這么去數(shù)《偬欤可以用 precomposedStringWithCanonicalMapping: 把字符串正規(guī)化成 C 形式(合成形式)來得到更好的結(jié)果:

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

    不巧的是堂氯,并不是所有情況都能這樣做,因?yàn)橹挥凶畛R姷慕M合字符序列有合成形式——其它基礎(chǔ)字符與標(biāo)記的組合即便是經(jīng)過正規(guī)化后露氮,也會(huì)保持原樣祖灰。如果你想知道字符串真正的字符個(gè)數(shù)钟沛,你只能遍歷字符串自己數(shù)畔规。后面循環(huán)那一節(jié)會(huì)繼續(xù)討論有關(guān)細(xì)節(jié)。

  3. 變體序列:它們和分解形式的組合字符序列的工作方式一樣恨统,因此變體選擇符也算作單獨(dú)的字符叁扫。

隨機(jī)訪問

characterAtIndex: 方法以索引方式直接訪問 unichar 會(huì)有同樣的問題。字符串可能會(huì)包含組合字符序列畜埋、代理對(duì)或變體序列莫绣。蘋果把這些都叫做合成字符序列(composed character sequence),這些術(shù)語就變得容易混淆悠鞍。注意不要把合成字符序列(蘋果的術(shù)語)和組合字符序列(Unicode 術(shù)語)搞混对室。后者是前者的子集】Ъ溃可以用 rangeOfComposedCharacterSequenceAtIndex: 來確定特定位置的 unichar 是不是代表單個(gè)字符(可能由多個(gè)碼點(diǎn)組成)的碼元序列的一部分掩宜。每當(dāng)給另一個(gè)方法傳入一個(gè)內(nèi)容未知的字符串的范圍作參數(shù)時(shí)都應(yīng)該這樣做,確保 Unicode 字符不會(huì)被從中間分開么翰。

循環(huán)

使用 rangeOfComposedCharacterSequenceAtIndex: 的時(shí)候牺汤,可以寫一個(gè)代碼套路來正確地循環(huán)字符串里所有的字符,但每次要遍歷一個(gè)字符串時(shí)都得這樣做太不方便了浩嫌。幸運(yùn)的是檐迟,NSString 有更好地方式:enumerateSubstringsInRange:options:usingBlock: 方法补胚。這個(gè)方法把 Unicode 抽象的地方隱藏了,能讓你輕松地循環(huán)字符串里的組合字符串追迟、單詞溶其、行、句子或段落怔匣。你甚至可以加上 NSStringEnumerationLocalized 這個(gè)選項(xiàng)握联,這樣可以在確定詞語間和句子間的邊界時(shí)把用戶所在的區(qū)域考慮進(jìn)去。要遍歷單個(gè)字符每瞒,把參數(shù)指定為 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));
}];

這個(gè)奇妙的方法表明金闽,蘋果想讓我們把字符串看做子字符串的集合,而不是(蘋果意義上的)字符的集合剿骨,因?yàn)?/p>

  1. 單個(gè) unichar 太小代芜,不足以代表一個(gè)真正的 Unicode 字符;
  2. 一些(普遍意義上的)字符由多個(gè) Unicode 碼點(diǎn)組成浓利。

請(qǐng)注意挤庇,這個(gè)方法的加入相對(duì)晚一些(在 OS X 10.6 和 iOS 4.0 的時(shí)候)。在之前贷掖,按字符循環(huán)一個(gè)字符串要麻煩得多嫡秕。

比較

除非你手動(dòng)執(zhí)行,否則字符串對(duì)象不會(huì)自己正規(guī)化苹威。這意味著直接比較包含組合字符序列的字符串可能會(huì)得出錯(cuò)誤的結(jié)果昆咽。isEqual:isEqualToString: 這兩個(gè)方法都是一個(gè)字節(jié)一個(gè)字節(jié)地比較的。如果希望字符串的合成和分解的形式相吻合牙甫,得先自己正規(guī)化:

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 é

另一個(gè)選擇是使用 compare: 方法(或者它的其它變形方法掷酗,比如:localizedCompare:),這個(gè)方法返回一個(gè)和它相容等價(jià)的字符串窟哺。對(duì)此泻轰,蘋果沒有很好地寫入文檔。請(qǐng)注意且轨,你常常還需要作標(biāo)準(zhǔn)等價(jià)的比較浮声。compare: 沒法作這個(gè)比較。

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

如果你只想用 compare: 比較而不考慮等價(jià)關(guān)系旋奢,compare:options 這個(gè)方法變體可以讓你指定 NSLiteralSearch 作為參數(shù)泳挥,這能讓比較更快。

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

總地來說黄绩,只有當(dāng)你知道文本所用的編碼時(shí)文本數(shù)據(jù)才是有用的羡洁。當(dāng)從服務(wù)器下載文本數(shù)據(jù)時(shí),通常你都知道或者可以從 HTTP 的頭文件中得知編碼類型爽丹。之后筑煮,再用 -[NSString initWithData:encoding:] 這個(gè)方法創(chuàng)建字符串對(duì)象就很簡(jiǎn)單了辛蚊。

編者注 這一段和下一段的這兩個(gè) NSString 的方法均為實(shí)例方法而非類方法,即應(yīng)該先 alloc 后再調(diào)用真仲,原文這樣寫估計(jì)只是為了簡(jiǎn)潔袋马,請(qǐng)讀者知會(huì)。

雖然文本文件本身并不包含編碼信息秸应,但 NSString 常陈橇荩可以通過查看擴(kuò)展文件屬性(extended file attributes)或者通過規(guī)律進(jìn)行試探性的猜測(cè)的方法(比如,一個(gè)有效的 UTF-8 文件里就不會(huì)出現(xiàn)某些特定的二進(jìn)制序列)來確定文件的編碼软啼∩5可以使用 -[NSString initWithContentsOfURL:encoding:error:] 這個(gè)方法,來從編碼已知的文件里讀取文本祸挪。要讀取編碼未知的文件锣披,蘋果提出了以下原則:

如果你不得不猜測(cè)文件的編碼(注意,沒有明確信息贿条,就只有猜測(cè)):

  1. 試試這兩個(gè)方法:stringWithContentsOfFile:usedEncoding:error: 或者 initWithContentsOfFile:usedEncoding:error: (或者這兩個(gè)方法參數(shù)為 URL 的等價(jià)方法)雹仿。
    這些方法會(huì)嘗試猜測(cè)資源的編碼,如果猜測(cè)成功整以,會(huì)以引用的形式帶回所用的編碼胧辽。
  2. 如果 1 失敗了,試著用 UTF-8 讀取資源公黑。
  3. 如果 2 失敗了邑商,試試合適的老的編碼。
    這里「合適的」取決于具體情況帆调。它可以是默認(rèn)的 C 語言字符串編碼奠骄,也可以是 ISO 或者 Windows Latin 1 編碼豆同,亦或者是其它的番刊,取決于你的數(shù)據(jù)來源。
  4. 最終影锈,還可以試試 Application Kit 里 NSAttributedString 類的載入方法(比如:initWithFileURL:options:documentAttributes:error:)芹务。這些方法會(huì)嘗試純文本文件,然后返回使用的編碼鸭廷≡姹В可以用這些方法打開任意的文檔。如果你的程序并不是專業(yè)處理文本的程序辆床,這些方法也值得考慮佳晶。對(duì)于 Foundation 級(jí)別的工具,或者不是自然語言的文本來說讼载,這些方法可能不太合適轿秧。

把文本寫入文件

我已經(jīng)提到過中跌,純文本文件,和文件格式或者網(wǎng)絡(luò)協(xié)議應(yīng)該選擇 UTF-8 編碼菇篡,除非有特別的需要只能用其它的編碼漩符。要向文件中寫入文本,使用 writeToURL:atomically:encoding:error: 這個(gè)方法驱还。

這個(gè)方法會(huì)在 UTF-16 或 UTF-32 編碼的文件上自動(dòng)加上字節(jié)順序標(biāo)記嗜暴。它還會(huì)把文件的編碼存儲(chǔ)在名為 com.apple.TextEncoding擴(kuò)展文件屬性里。鑒于 initWithContentsOf…: usedEncoding:error: 方法知道有這個(gè)屬性议蟆,當(dāng)你從文件里載入文本時(shí)闷沥,使用標(biāo)準(zhǔn)的 NSString 方法就能讓確保使用正確的編碼更加容易。

原文 NSString與Unicode
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末咐容,一起剝皮案震驚了整個(gè)濱河市狐赡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌疟丙,老刑警劉巖颖侄,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異享郊,居然都是意外死亡览祖,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門炊琉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來展蒂,“玉大人,你說我怎么就攤上這事苔咪∶痰浚” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵团赏,是天一觀的道長(zhǎng)箕般。 經(jīng)常有香客問我,道長(zhǎng)舔清,這世上最難降的妖魔是什么丝里? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮体谒,結(jié)果婚禮上杯聚,老公的妹妹穿的比我還像新娘。我一直安慰自己抒痒,他們只是感情好幌绍,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般傀广。 火紅的嫁衣襯著肌膚如雪痢虹。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天主儡,我揣著相機(jī)與錄音奖唯,去河邊找鬼。 笑死糜值,一個(gè)胖子當(dāng)著我的面吹牛丰捷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播寂汇,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼病往,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了骄瓣?” 一聲冷哼從身側(cè)響起停巷,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎榕栏,沒想到半個(gè)月后畔勤,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡扒磁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年庆揪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片妨托。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡缸榛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出兰伤,到底是詐尸還是另有隱情内颗,我是刑警寧澤,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布敦腔,位于F島的核電站弯院,受9級(jí)特大地震影響历葛,放射性物質(zhì)發(fā)生泄漏酬屉。R本人自食惡果不足惜韭赘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一筒捺、第九天 我趴在偏房一處隱蔽的房頂上張望柏腻。 院中可真熱鬧,春花似錦系吭、人聲如沸五嫂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽沃缘。三九已至躯枢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間槐臀,已是汗流浹背锄蹂。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留水慨,地道東北人得糜。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像晰洒,于是被迫代替她去往敵國(guó)和親朝抖。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

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