objc系列譯文(9.2):玩轉字串

發表於2014-03-11

在每個應用裡我們都大量使用字串。下面我們將快速看看一些常見的操作字串的方法,過一遍常見操作的最佳實踐。

字串的比較、搜尋和排序

排序和比較字串比第一眼看上去要複雜得多。不只是因為字串可以包含代理對(surrogate pairs )(詳見 Ole 寫的這篇關於 Unicode 的文章) ,而且比較還與字串的本地化相關。在某些極端情況下相當棘手。

蘋果文件中 String Programming Guide 裡有一節叫做 “字元與字形叢集(Characters and Grapheme Clusters)”,裡面提到一些陷阱。例如對於排序來說,一些歐洲語言將序列“ch”當作單個字母。在一些語言裡,“ä”被認為等同於 ‘a’ ,而在其它語言裡它卻被排在 ‘z’ 後面。

而 NSString 有一些方法來幫助我們處理這種複雜性。首先看下面的方法:

它帶給我們充分的靈活性。另外,還有很多“便捷函式”都使用了這個方法。

與比較有關的可用引數如下:

它們都可以用邏輯或運算組合在一起。

NSCaseInsensitiveSearch :“A”等同於“a”,然而在某些地方還有更復雜的情況。例如,在德國,“ß” 和 “SS”是等價的。

NSLiteralSearch :Unicode 的點對 Unicode 點比較。它只在所有字元都用相同的方式組成的情況下才會返回相等。LATIN CAPITAL LETTER A 加上 COMBINING RING ABOVE 並不等同於 LATIN CAPITAL LETTER A WITH RING ABOVE.

譯註:這個要解釋一下,首先,每一個Unicode都是有官方名字的!LATIN CAPITAL LETTER A是一個大寫“A”,COMBINING RING ABOVE是一個  ̊,LATIN CAPITAL LETTER A WITH RING ABOVE,這是Å前兩者的組合不等同於後者。

NSNumericSearch:它對字串裡的數字排序,所以 “Section 9” < “Section 20” < “Section 100.”

NSDiacriticInsensitiveSearch : “A” 等同於 “Å” 等同於 “Ä.”

NSWidthInsensitiveSearch : 一些東亞文字(平假名 和 片假名)有全寬與半寬兩種形式。

很值得一提的是 – (NSComparisonResult)localizedStandardCompare: ,它排序的方式和 Finder 一樣。它對應的選項是 NSCaseInsensitiveSearch 、 NSNumericSearch 、NSWidthInsensitiveSearch 以及 NSForcedOrderingSearch 。如果我們要在UI上顯示一個檔案列表,用它就最合適不過了。

大小寫不敏感的比較和音調符號不敏感的比較都是相對複雜和昂貴的操作。如果我們需要比較很多次字串那這就會成為一個效能上的瓶頸(例如對一個大的資料集進行排序),一個常見的解決方法是同時儲存原始字串和摺疊字串。例如,我們的 Contact  類有一個正常的 name  屬性,在內部它還有一個foldedName  屬性,它將自動在 name變化時更新。那麼我們就可以使用 NSLiteralSearch  來比較 name  的摺疊版本。 NSString  有一個方法來建立摺疊版本:

搜尋

要在一個字串中搜尋子字串,最靈活性的方法是:

同時,還有一些“便捷方法”,它們在最終都會呼叫上面這個方法,我們可以傳入上面列出的引數,以及以下這些額外的引數:

NSBackwardsSearch :在字串的末尾開始反向搜尋。

NSAnchoredSearch : 只考慮搜尋的起始點(單獨使用)或終止點(當與 NSBackwardsSearch  結合使用時)。這個方法可以用來檢查字首或者字尾,以及大小寫不敏感(case-insensitive)或者音調不敏感(diacritic-insensitive)的比較。

NSRegularExpressionSearch :使用正規表示式搜尋,要了解更多與使用正規表示式有關的資訊,請關注 Chris’s 的 String Parsing 。

另外,還有一個方法:

與前面搜尋字串不同的是, 它只搜尋給定字符集的第一個字元。即使只搜尋一個字元,但如果由於此字元是由元字元組成的序列(composed character sequence),所以返回範圍的長度也可能大於1。

大寫與小寫

一定不要使用 NSString  的 -uppercaseString  或者 -lowercaseString  的方法來處理 UI 顯示的字串,而應該使用 -uppercaseStringWithLocale  來代替, 比如:

格式化字串

同C語言中的 sprintf 函式( ANSI C89 中的一個函式 )類似, Objective C 中的 NSString 類也有如下的3個方法:

需要注意這些格式化方法都是 非本地化 的 。所以這些方法得到的字串是不能直接拿來顯示在使用者介面上的。如果需要本地化,那我們需要使用下面這些方法:

Florian 有一篇關於 字串的本地化 的文章更詳細地討論了這個問題。

printf(3)的man頁面有關於它如何格式化字串的全部細節。除了所謂的轉換格式(它以%字元開始),格式化字串會被逐字複製:

我們格式化了兩個浮點數。注意單精度浮點數和雙精度浮點數共同了一個轉換格式。

物件

除了來自 printf(3) 的轉換規範,我們還可以使用 %@  來輸出一個物件。在物件描述那一節中有述,如果物件響應 -descriptionWithLocale:  方法,則呼叫它,否則呼叫 -description 。  %@  被結果替換。

整數

使用整形數字時,有些需要注意的細節。首先,有符號數(d和i)和無符號數(o、u、x和X)分別有轉換規範。需要使用者選擇具體的型別。

如果我們使用的東西是 printf不知道的,我們必須要做型別轉換。 NSUInteger  正是這樣一個例子,它在64位和32位平臺上是不一樣的。下面的例子可以同時工作在32位和64位平臺。

Modifier d, i o, u, x, X
hh   signed char   unsigned char
h   short   unsigned short
(none)   int   unsigned int
l (ell)   long   unsigned long
ll (ell ell)   long long   unsigned long long
j   intmax_t   uintmax_t
t   ptrdiff_t
z   size_t

適用於整數的轉換規則有:

%d  和 %i  具有一樣的功能,它們都列印出有符號十進位制數。 %o  就較為晦澀了:它使用八進位制表示。 %u  輸出無符號十進位制數——它是我們常用的。最後 %x  和 %X  使用十六進位制表示——後者使用大寫字母。

對於 x%  和 X%  ,我們可以在 0x 前面新增 “#” 井字元字首看,增加可讀性。

我們可以傳入特定引數,來設定最小欄位寬度和最小數字位數(預設兩者都是0),以及左/右對齊。請檢視man頁面獲取詳細資訊。下面是一些例子:

%p  可用於列印出指標——它和 %#x  相似但可同時在32位和64位平臺上正常工作。

浮點數

關於浮點數的轉換規則有8個:eEfFgGaA。但除了 %f 和 %g 外我們很少使用其它的。對於指數部分,小寫的版本使用小寫 e,大寫的版本就使用大寫 E。

通常 %g  是浮點數的全能轉換符 ,它與 %f  的不同在下面的例子裡顯示得很清楚:

和整數一樣,我們依然可以指定最小欄位寬度和最小數字數。

指定位置

格式化字串允許使用引數來改變順序:

我們只需將從1開始的引數與一個$接在%後面。這種寫法在進行本地化的時候極其常見,因為在不同語言中,各個引數所處的順序位置可能不盡相同。

NSLog()

NSLog() 函式與  +stringWithFormat: 的工作方式一樣。我們可以呼叫:

下面的程式碼可以用同樣的方式構造字串:

顯然  NSLog()會輸出字串,並且它會加上時間戳、程式名、程式ID以及執行緒ID作為字首。

實現能接受格式化字串的方法

有時在我們自己的類中提供一個能接受格式化字串的方法會很方便使用。假設我們要實現的是一個 To Do 應用,它包含一個 Item 類。我們想要提供:

如此我們就可以使用:

這種型別的方法可以接受可變數量的引數,所以被稱為可變引數方法。我們必須使用一個定義在stdarg.h裡的巨集來使用可變引數。上面方法的實現程式碼可能會像下面這樣:

進一步,我們要新增 NS_FORMAT_FUNCTION 到方法的定義裡(在標頭檔案中),如下所示:

NS_FORMAT_FUNCTION 展開為一個方法 __attribute__,它會告訴編譯器在索引1處的引數是一個格式化字串,而實際引數從索引2開始。這將允許編譯器檢查格式化字串而且會像 NSLog() 和 -[NSString stringWithFormat:] 一樣輸出警告資訊。

字元與字串元件

如有一個字串 “bird” ,找出組成它的獨立字母是很簡單的。第二個字母是“i”(Unicode: LATIN SMALL LETTER I)。而對於像Åse這樣的字串就沒那麼簡單了。看起來像三個字母的組合可有多種方式,例如:

或者

從 Ole 寫的這篇關於 Unicode 的文章 裡可以讀到更多關於聯合標記(combining marks)的資訊,其他語言文字有更多複雜的代理對(complicated surrogate pairs)

如果我們要在字元層面處理一個字串,那我們就要小心翼翼。蘋果官方文件中 String Programming Guide 有一節叫做 “Characters and Grapheme Clusters”,裡面有更多關於這一點的細節。

NSString有兩個方法:

上面這兩個方法在有的時候很有幫助,例如,分開一個字串時保證我們不會分開被稱為代理對(surrogate pairs)的東西。

如果我們要在字串的字元上做工作, NSString 有個叫做 -enumerateSubstringsInRange:options:usingBlock: 的方法。

將 NSStringEnumerationByComposedCharacterSequences 作為選項傳遞,我們就能掃描所有的字元。例如,用下面的方法,我們可將字串 “International Business Machines” 變成 “IBM”。

如文件所示,詞和句的分界可能基於地區的變化而變化。因此有 NSStringEnumerationLocalized選項。

多行文字字面量

編譯器的確有一個隱蔽的特性:把空格分隔開的字串銜接到一起。這是什麼意思呢?下面兩段程式碼是完全等價的:

前者看起來更舒服,但是有一點要注意千萬不要在任意一行末尾加入逗號或者分號。

同時也可以這樣做:

*譯者注:上面這行程式碼原文是有誤的,原文是 NSString * @”The man ” @”who knows everything ” @”learns nothing” @”.”  ,讀者可以嘗試一下,如果這樣寫是無法通過編譯的;

編譯器只是為我們提供了一個便捷的方式,將多個字串在編譯期組合在了一起。

可變字串

可變字串有兩個常見的使用場景:(1)拼接字串(2)替換部分字串

建立字串

可變字串可以很輕易地把多個字串在你需要的時候組合起來。

這裡要注意的是,雖然原本返回值應該是一個 NSString  型別的物件,我們只是簡單地返回一個NSMutableString 型別的物件。

替換字串

可變字串除了追加組合之外,還提供了以下4個方法:

這些方法和 NSString 的類似:

但是它沒有建立新的字串僅僅把當前字串變成了一個可變的型別,這樣讓程式碼更容易閱讀,以及提升些許效能。

 連線元件

一個看似微不足道但很常見的情況是字串連線。比如現在有這樣幾個字串:

我們想用它們來建立下面這樣的一個字串:

那麼就可以這樣做:

如果我們將其顯示給使用者,我們就要使用本地化表達,確保將最後一部分替換相應語言的 “, and” :

那麼在本地化的時候,如果是英語,應該是:

如果是德語,則應該是:

結合元件的逆過程可以用   -componentsSeparatedByString: ,這個方法會將一個字串變成一個陣列。例如,將 “12|5|3” 變成 “12”、“5” 和 “3”。

物件描述

在許多物件導向程式語言裡,物件有一個叫做 toString() 或類似的方法。在 Objective C 裡,這個方法是:

以及它的兄弟方法:

當自定義模型物件時,覆寫 -description 方法是一個好習慣,在UI上顯示該物件時呼叫的就是description方法的返回值。假定我們有一個 Contact類,下面是它的 description方法實現。

我們可以像下面程式碼這樣格式化字串:

因為該字串是用來做UI顯示的,我們可能需要做本地化,那麼我們就需要覆寫descriptionWithLocale:(NSLocale *)locale方法。

%@ 會首先呼叫 -descriptionWithLocale,如果沒有返回值,再呼叫 -description,在除錯時,列印一個物件,我們用 po這個命令(它是print object的縮寫)

如果在除錯視窗的終端下輸入 po contact, 它會呼叫物件的 debugDescription方法。預設情況下debugDescription是直接呼叫 description。如果你希望輸出不同的資訊,那麼就分別覆寫兩個方法。大多數情況下,尤其是對於非資料模型的物件,你只需要覆寫 -description就能滿足需求了。

實際上物件的標準格式化輸出是這樣的:

NSObject就是這麼幹的。當你覆寫該方法時,也可以像這樣寫。假定我們有一個DetailViewController,在它的UI上要顯示一個 contact ,我們可能會這樣覆寫該方法:

 NSManagedObject子類的描述

我們將特別注意向 NSManagedObject 的子類新增 -description / -debugDescription 的情況。由於 Core Data的惰性載入機制(faulting mechanism)允許未載入資料的物件存在,所以當我們呼叫 -debugDescription 我們並不希望改變我們的應用程式的狀態,因此我要確保檢查 isFault  這個屬性。例如,我們可如下這樣實現它:

再次,因為它們是模型物件,過載 -description 簡單地返回描述例項的屬性名就可以了。

檔案路徑

簡單來說就是我們不應該使用 NSString來描述檔案路徑。對於 OS X 10.7 和 iOS 5, NSURL更便於使用,而且更有效率,它還能快取檔案系統的屬性。

再者, NSURL 有八個方法來訪問被稱為 resource values 的東西。它們提供給我們一個穩定的介面來獲取和設定檔案與目錄的多種屬性,例如本地化檔名( NSURLLocalizedNameKey)、檔案大小(NSURLFileSizeKey),以及建立日期( NSURLCreationDateKey),等等。

尤其是在遍歷目錄內容時,使用 -[NSFileManagerenumeratorAtURL:includingPropertiesForKeys:options:errorHandler:] 附帶一個關鍵詞列表,然後用 -getResourceValue:forKey:error: 檢索它們,能帶來顯著的效能提升。

下面是一個簡短的例子展示瞭如何將它們組合在一起:

我們把屬性的鍵傳給  -enumeratorAtURL: 方法中,在遍歷目錄內容時,這個方法能確保用非常高效的方式獲取它們。在迴圈中,呼叫 -getResourceValue:… 能簡單地從 NSURL 得到已快取的值,而不用去訪問檔案系統。

傳遞路徑到UNIX API

因為 Unicode 非常複雜,同一個字母有多種表示方式,所以我們需要很小心地傳遞路徑給UNIX API。在這些情況裡,一定不能使用 UTF8String ,正確地做法是使用 -fileSystemRepresentation 方法,如下:

與 NSURL 類似,同樣的情況也發生在 NSString 上。如果我們不這麼做,在開啟一個檔名或路徑名包含合成字元的檔案時我們將看到隨機錯誤。在 OS X 上,當使用者的短名剛好包含合成字元時就會顯得特別糟糕。

我們需要一個 char const * 版本的路徑的一些常見情況是UNIX open() 和 close() 指令。但這也可能發生在 GCD / libdispatch 的 I/O API 上。

如果我們要使用 NSString 來做,那我們要保證像下面這樣做:

-fileSystemRepresentation 所做的是它首先將這個字串轉換成檔案系統的規範形式然後用UTF-8編碼。

相關文章