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 特性后专。
-
基本多文種平面外的字符:記住,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
-
組合字符序列:如果字母 é 是以分解形式(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é)。
變體序列:它們和分解形式的組合字符序列的工作方式一樣恨统,因此變體選擇符也算作單獨(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>
- 單個(gè)
unichar
太小代芜,不足以代表一個(gè)真正的 Unicode 字符; - 一些(普遍意義上的)字符由多個(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è)):
- 試試這兩個(gè)方法:
stringWithContentsOfFile:usedEncoding:error:
或者initWithContentsOfFile:usedEncoding:error:
(或者這兩個(gè)方法參數(shù)為 URL 的等價(jià)方法)雹仿。
這些方法會(huì)嘗試猜測(cè)資源的編碼,如果猜測(cè)成功整以,會(huì)以引用的形式帶回所用的編碼胧辽。- 如果 1 失敗了,試著用 UTF-8 讀取資源公黑。
- 如果 2 失敗了邑商,試試合適的老的編碼。
這里「合適的」取決于具體情況帆调。它可以是默認(rèn)的 C 語言字符串編碼奠骄,也可以是 ISO 或者 Windows Latin 1 編碼豆同,亦或者是其它的番刊,取決于你的數(shù)據(jù)來源。- 最終影锈,還可以試試 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
方法就能讓確保使用正確的編碼更加容易。