讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 10.4 Foundation類精講 >

10.4 Foundation類精講

Cocoa的Foundation類提供了一些基本數據類型與輔助方法,它們構成了使用Cocoa的基礎。顯然,我無法將其一一列舉,更不必說完整介紹它們了,但我可以介紹一些你在編寫最簡單的iOS程序前所需要瞭解的內容。要想瞭解更多信息,請從Foundation Framework Reference的Foundation類列表開始。

10.4.1 常用的結構體與常量

NSRange是個C結構體(參見附錄A),它對於處理我將要介紹的一些類是非常重要的。其組成都是整數,location與length。比如,location為1的NSRange表示從第2個元素開始(因為元素計數總是基於0的),如果length為2,那就表示這個元素與下一個。

Cocoa提供了各種便捷函數來處理NSRange;比如,你可以調用NSMakeRange通過兩個整數創建NSRange(注意到名字NSMakeRange向後類比於CGPointMake與CGRectMake等名字)。Swift通過將NSRange橋接為Swift結構體來解決遇到的問題。你可以對NSRange與Swift Range(端點為Int)進行轉換:Swift為NSRange增加了一個初始化器,它接收一個Swift Range,另外還有一個toRange方法。

NSNotFound是個整型常量,表示找不到所請求的元素。NSNotFound真正的數值是什麼並不重要;只要與NSNotFound本身進行比較即可,從而判斷結果是否有意義。比如,如果獲取某個對象在NSArray中的索引,但該對像不存在,那麼結果就是個NSNotFound:


let arr = ["hey"] as NSArray
let ix = arr.indexOfObject("ho")
if ix == NSNotFound {
    print("it wasn't found")
}
  

Cocoa為何要以這種方式依賴於擁有特殊含義的整型值呢?這是因為它只能這麼做。表示對像不存在的結果不能為0,因為0表示數組的第一個元素。結果也不能是-1,因為NSArray索引值總是正數。它也不能為nil,因為當需要返回一個整數時,Objective-C不能返回nil(即便可以,那它也會被看作0的另一種表示方式)。相反,Swift的indexOf方法會返回一個包裝了Int的Optional,這樣就可以返回nil來表示目標對像沒有找到了。

如果搜索返回一個範圍,但並沒有找到任何結果,那麼生成的NSRange的location就為NSNotFound。第3章曾經介紹過,Swift有時會自動幫你做一些聰明且自動化的橋接工作,從而無需再與NSNotFound進行比較了。典型示例就是NSString的rangeOfString:方法。在Cocoa的定義中,它會返回一個NSRange;Swift則將其改造為返回一個包裝了Swift Range(String.Index)的Optional,如果NSRange的location為NSNotFound,那就會返回nil:


let s = "hello"
let r = s.rangeOfString("ha") // nil; an Optional wrapping a Swift Range
  

如果你需要的是個Swift Range,那就正合適,適合於進一步切割Swift String;但如果需要的是個NSRange,想要返回給Cocoa,那就需要將原來的Swift String轉換為NSString,這樣結果依然是Cocoa類:


let s = " hello" as NSString
let r = s.rangeOfString("ha") // an NSRange
if r.location == NSNotFound {
    print("it wasn't found")
}
  

10.4.2  NSString及相關類

NSString是字符串的Cocoa對像版本。NSString與Swift String會彼此橋接,你常常會不自覺地在這兩者間切換,需要NSString時就將Swift String傳遞給Cocoa,在Swift String上調用Cocoa NSString方法,諸如此類。比如:


let s = "hello"
let s2 = s.capitalizedString
  

在上述代碼中,s是個Swift String,s2也是個Swift String,但capitalizedString屬性實際上是Cocoa的。在執行上述代碼時,Swift String會被橋接到NSString並傳遞給Cocoa,Cocoa則會處理它並得到大寫的字符串;這個大寫的字符串是個NSString,不過它可以橋接到Swift String。你基本意識不到橋接過程;capitalizedString就像是原生String的屬性一樣,不過它並不是,可以在沒有導入Foundation的環境下使用它來證明這一點(其實是不行的)。

在某些情況下,你需要進行顯式類型轉換來橋接。Swift可能會在橋接時失敗;比如,如果s是個Swift字符串,那你就不能直接對其調用stringByAppendingPathExtension::


let s = "MyFile"
let s2 = s.stringByAppendingPathExtension("txt") // compile error
  

你需要顯式將其轉換為NSString:


let s2 = (s as NSString).stringByAppendingPathExtension("txt")
  

此外,對字符串使用索引時會出現問題。比如:


let s = "hello"
let s2 = s.substringToIndex(4) // compile error
  

問題在於橋接是你自己做的。Swift並不會阻止你在Swift String上調用substring-ToIndex:方法,不過索引值必須是個String.Index,這很難構建(參見第3章):


let s2 = s.substringToIndex(s.startIndex.advancedBy(4))
  

如果不想這麼做,那就需要提前將String強制類型轉換為NSString;現在處理的都是Cocoa了,字符串索引是整型值:


let s2 = (s as NSString).substringToIndex(4)
  

不過,正如第3章所介紹的那樣,這兩個調用實際上並不是等價的:其結果是不同的!原因在於從根本上來說,String與NSString在字符串的元素構成上擁有完全不同的表示方式。String會將元素解析為字符,這意味著它會遍歷字符串,將任何可合併的代碼點聚合起來;NSString的行為就好像它是個UTF16代碼點的數組。從Swift的角度來看,String.Index的增加都對應於真正的字符,不過通過索引或範圍訪問卻需要遍歷字符串;從Cocoa的角度來看,通過索引或範圍訪問是非常快的,不過可能無法對應上字符邊界(參見Apple String Programming Guide的「Characters and Grapheme Clusters」一章)。

Swift String與Cocoa NSString之間的另一個主要差別在於NSString是不可變的。這意味著對於NSString,你可以做到根據一個字符串來獲得另一個新的字符串(就像capitalizedString與substringToIndex:所做的那樣),不過不能就地修改字符串。要想做到這一點,你需要另一個類NSMutableString,它是NSString的子類。NSMutableString有很多有用的方法,你可以充分利用這些方法;不過Swift String並沒有橋接到NSMutableString,因此無法僅通過類型轉換將String轉換為NSMutableString。要想得到NSMutableString,你需要創建一個。最簡單的方式是使用NSMutableString的初始化器init(string:),它接收一個NSString,這意味著你可以傳遞一個Swift String進去。這樣,只需一步就可以將NSMutableString轉換為Swift String。


let s = "hello"
let ms = NSMutableString(string:s)
ms.deleteCharactersInRange(NSMakeRange(ms.length-1,1))
let s2 = (ms as String) + "ion" // now s2 is a Swift String
  

正如第3章所介紹的,原生Swift String方法數量並不多。所有的字符串處理能力都依賴於橋接的另一方Cocoa。因此,你會經常通過橋接完成一些功能!這並不是只針對NSString與NSMutableString類的。很多其他的常見類都與之相關。

比如,假設要查找某個字符串中的子字符串。最佳做法都來自於Cocoa:

·可以通過各種rangeOfString:...方法搜索NSString,同時還可以使用大量的選項,比如,忽略臨界值、忽略大小寫、從尾部開始,以及待搜索的子字符串一定要位於被搜索字符串的起始或結束位置處。

·也許不太確定要搜索的是什麼:你需要描述出其結構。可以通過NSScanner遍歷字符串,查找滿足某些條件的子字符串;比如,借助NSScanner(以及NSCharacterSet),你可以跳過以數字開頭的子字符串並提取出數字。

·通過指定選項.RegularExpressionSearch,你可以使用正則表達式搜索。正則表達式也是通過單獨一個類NSRegularExpression得到支持的,它會使用NSTextCheckingResult描述匹配結果。

·更加複雜的自動化文本分析是通過其他一些類得到支持的,比如,NSDataDetector,它是NSRegularExpression的子類,可以迅速找到某些類型的字符串表達式,如URL或電話號碼;還有NSLinguisticTagger,它會根據文法詞性規則分析文本。

在該示例中,我們要將所有「hello」替換為「heaven」。我們並不希望見到子字符串「hell」就替換,比如,「hello」就不應該替換。搜索需要智能一些,知道單詞的邊界是什麼。這看起來是正則表達式的事情。Swift並沒有提供正則表達式支持,因此一切都要通過Cocoa來完成:


let s = NSMutableString(string:"hello world, go to hell")
let r = try! NSRegularExpression(
    pattern: "\\bhell\\b",
    options: .CaseInsensitive)
r.replaceMatchesInString(
    s, options: , range: NSMakeRange(0,s.length),
    withTemplate: "heaven")
// s is "hello world, go to heaven"
  

NSString還提供了一些便捷的功能用以處理文件路徑字符串,常用於NSURL,這是另一個值得探究的Foundation類。此外,NSString(就像本節介紹的其他類一樣)提供了寫到文件以及從文件讀取的方法;可以通過NSString文件路徑或NSURL來指定文件。

NSString並沒有字體與大小等信息。顯示字符串的界面對像(如UILabel)有一個類型為UIFont的font屬性;不過,它只用於確定該組件上所顯示的字符串的字體與大小。如果需要帶樣式的文本(不同的文本有不同的樣式屬性,如大小、字體及顏色等),那麼可以使用NSAttributedString及其支持類:NSMutableAttributedString、NSParagraphStyle與NSMutableParagraphStyle。你可以通過它們從各個方面為文本和段落增加樣式。顯示文本的內建界面對象可以顯示NSAttributedString。

可以通過NSString(參見String UIKit Additions Reference)及NSAttributedString(參見NSAttributedString UIKit Additions Reference)上的NSStringDrawing類別所提供的方法在圖形上下文中繪製字符串。

10.4.3  NSDate及相關類

NSDate是個日期與時間,內部表示為從某個參考日期開始所經過的秒數(NSTimeInterval)。調用NSDate的初始化器init()(即NSDate())會生成一個代表當前日期與時間的日期對象。很多日期操作還會用到NSDateComponents,NSDate與NSDateComponents之間的轉換需要傳遞一個NSCalendar。下述示例展示了如何根據日曆值構建一個日期:


let greg = NSCalendar(calendarIdentifier:NSCalendarIdentifierGregorian)!
let comp = NSDateComponents
comp.year = 2016
comp.month = 8
comp.day = 10
comp.hour = 15
let d = greg.dateFromComponents(comp) // Optional wrapping NSDate
  

與之類似,NSDateComponents提供了進行日期計算的正確方式。如下示例展示了如何為給定的日期增加一個月:


let d = NSDate // or whatever
let comp = NSDateComponents
comp.month = 1
let greg = NSCalendar(calendarIdentifier:NSCalendarIdentifierGregorian)!
let d2 = greg.dateByAddingComponents(comp, toDate:d, options:)
  

你可能還會考慮以字符串表示的日期。如果不對日期的字符串表示進行顯式的處理,那麼其字符串表示格式會讓你吃驚的。比如,如果print一個NSDate,那麼它會以GMT時區的形式表示日期,如果不在這兒住,那麼這個結果會讓你感到困惑。一個簡單的解決辦法就是調用descriptionWithLocale:;它會考慮到用戶當前的時區、語言、區域格式以及日曆設置等:


print(d)
// 2016-08-10 22:00:00 +0000
print(d.descriptionWithLocale(NSLocale.currentLocale))
// Wednesday, August 10, 2016 at 3:00:00 PM Pacific Daylight Time
  

請使用NSDateFormatter來創建並解析日期字符串,它使用了類似於NSLog(以及NSString的stringWithFormat:)的格式化字符串。在該示例中,我們完全使用了用戶的區域設置,通過dateFormatFromTemplate:options:locale:與當前區域設置生成一個NSDateFormatter。「模板」是個字符串,列出了將要使用的日期組件,不過其順序、標點符號和語言則留給區域設置來處理:


let df = NSDateFormatter
let format = NSDateFormatter.dateFormatFromTemplate(
    "dMMMMyyyyhmmaz", options:0, locale:NSLocale.currentLocale)
df.dateFormat = format
let s = df.stringFromDate(NSDate)
  

生成的日期會使用用戶的時區和語言,並使用正確的語言規範。這涉及區域設置格式與語言的組合,它們是兩個單獨的設置。這樣:

·在我的設備上,結果是「July 16,2015,7:44 AM PDT.」。

·如果將設備的區域設置修改為France,那麼結果就變成了「16 July 20157:44 AM GMT-7.」。

·如果再將設備的語言修改為French,那麼結果又會變成「16 juillet 20157:44 AM UTC-7.」。

10.4.4  NSNumber

NSNumber是個包裝了數值的對象。被包裝的值可以是任何標準的Objective-C數值類型(包括BOOL,這是Objective-C中與Swift Bool的對應類型)。讓Swift用戶感到驚訝的是竟然還需要NSNumber。不過,Objective-C中的普通數字並不是對像(它是標量,參見附錄A),因此無法在需要對象的地方使用。這樣,NSNumber就解決了一個重要問題,可以將數字轉換為對象,反之亦然。

Swift會盡一切努力不讓你直接使用NSNumber。它通過兩種不同方式橋接了Swift數值類型與Objective-C:

·如果需要普通的數字,那麼Swift數字就會橋接到普通的數字(標量)。

·如果需要對象,那麼基本的數字類型的Swift數字就會橋接到NSNumber。基本的數字類型有Int、UIInt、Float、Double以及Bool,因為NSNumber能夠包裝Objective-C BOOL。

看看下面這個示例:


let ud = NSUserDefaults.standardUserDefaults
let i = 0
ud.setInteger(i, forKey: "Score") 1
ud.setObject(i, forKey: "Score") 2
  

後兩行看起來很像,不過Swift對待Int值i的方式卻是不同的:

1setInteger:forKey:的第1個參數需要一個整型(標量),因此Swift會將Int結構體值i轉換為普通的Objective-C數字。

2setObject:forKey:的第1個參數需要一個對象,因此Swift會將Int結構體值i轉換為NSNumber。

自然,如果想要顯式跨過這種橋接,那也是可行的。可以將Swift數字(基本的數字類型)強制轉換為NSNumber:


let n = 0 as NSNumber
  

要想更好地控制NSNumber所包裝的數值類型,你可以調用NSNumber的初始化器:


let n = NSNumber(float:0)
  

從Objective-C回到Swift,值一般會作為AnyObject,你需要進行向下類型轉換。NSNumber擁有一些屬性可根據數字類型訪問被包裝的值。回憶一下第5章的示例,它會從NSNotification的userInfo字典中將值提取出來並作為NSNumber返回:


if let prog = (n.userInfo?["progress"] as? NSNumber)?.doubleValue {
    self.progress = prog
}
  

NSNumber還可以向下類型轉換為Swift數值類型。因此,包裝NSNumber的AnyObject也可以。這樣,該示例可以改寫成下面這樣,不會顯式用到NSNumber:


if let prog = n.userInfo?["progress"] as? Double {
    self.progress = prog
}
  

在第2個版本中,Swift實際上在背後做的是與第1個示例相同的事情,將AnyObject當作NSNumber,並通過doubleValue屬性提取出被包裝的數字。

NSNumber對像只是一個包裝器而已。無法將其直接用在數字計算上;它並非數字,而是包裝了一個數字。不管怎樣,如果需要數字,那就需要從NSNumber中提取。

另外,NSNumber的子類NSDecimalNumber可用於計算,這多虧了大量的數學計算方法:


let dec1 = NSDecimalNumber(float: 4.0)
let dec2 = NSDecimalNumber(float: 5.0)
let sum = dec1.decimalNumberByAdding(dec2) // 9.0
  

NSDecimalNumber在取整方面非常有用,因為它提供了一種便捷的方式來指定所需的取整方式。

NSDecimalNumber底層是NSDecimal結構體(它是NSDecimalNumber的decimalValue)。

NSDecimal擁有一些C函數,速度上要比NSDecimalNumber方法快很多。

10.4.5  NSValue

NSValue是NSNumber的父類。它用於在需要對象的時候包裝非數字的C值,比如,C結構體。它所解決的問題類似於NSNumber:Swift結構體是個對象,不過C結構體不是,因此在Objective-C中,如果需要對象,那麼使用結構體是行不通的。

可以通過NSValue上的NSValueUIGeometryExtensions類別所提供的便捷方法(參見NSValue UIKit Additions Reference)輕鬆包裝和展開CGPoint、CGSize、CGRect、CGAffineTransform、UIEdgeInsets與UIOffset;還有其他一些類別可以輕鬆包裝和展開NSRange、CATransform3D、CMTime、CMTimeMapping、CMTimeRange、MKCoordinate與MKCoordinateSpan。一般不需要在NSValue中存儲其他類型的C值,不過如果需要也是可以的。

Swift並不會神奇地橋接這些C結構體類型與NSValue。你需要顯式對其進行管理,正如使用Objective-C代碼所做的那樣。如下示例使用Core Animation實現界面上的按鈕從一個位置到另一個位置的移動;按鈕的起止位置都表示為CGPoint,不過動畫的fromValue與toValue必須是對象。CGPoint並非Objective-C對象,因此需要將CGPoint值包裝到NSValue對像中:


let ba = CABasicAnimation(keyPath:"position")
ba.duration = 10
ba.fromValue = NSValue(CGPoint:self.oldButtonCenter)
ba.toValue = NSValue(CGPoint:goal)
self.button.layer.addAnimation(ba, forKey:nil)
  

與之類似,可以在Swift中創建CGPoint的數組,這是因為CGPoint變成了一個Swift對像類型(Swift結構體),而Swift Array可以持有任意類型的元素;不過不能將該數組傳遞給Objective-C,因為Objective-C NSArray中的元素必須是對象,而Objective-C中的CGPoint並不是對象。這樣,首先就需要將CGPoints包裝到NSValue對像中。下面是另一個動畫示例,我通過將CGPoints數組轉換為NSValues數組來設置關鍵幀動畫的values數組(NSArray)。


anim.values = [oldP,p1,p2,newP].map{NSValue(CGPoint:$0)}
  

10.4.6  NSData

NSData是個字節序列;基本上,它是個緩存,佔據了一塊內存。它是不可變的;其可變版本是其子類NSMutableData。

在實際開發中,NSData主要用在如下兩種情況當中:

·從Internet上下載數據。比如,NSURLConnection與NSURLSession會將從Internet上接收到的東西當作NSData。你可以根據需要將其轉換為字符串,並指定正確的編碼。

·將對像存儲為文件或用戶首選項(NSUserDefaults)。比如,你無法直接將UIColor值存儲為用戶首選項。如果用戶選擇了某個顏色,你需要將其保存起來,那麼你可以將UIColor轉換為NSData(使用NSKeyedArchiver)並保存:


let ud = NSUserDefaults.standardUserDefaults
let c = UIColor.blueColor
let cdata = NSKeyedArchiver.archivedDataWithRootObject(c)
ud.setObject(cdata, forKey: "myColor")
  

10.4.7 相等與比較

在Swift中,如果對像類型使用了Equatable與Comparable協議,那麼我們就可以針對該對像類型重寫相等與比較運算符。不過Objective-C運算符則不行,在Objective-C中,相等與比較運算符只能用於標量。

要想對兩個對像進行「相等」判斷(無論對於該對像類型來說相等意味著什麼),Objective-C類必須要實現isEqual:,它繼承自NSObject。Swift則會將NSObject看作Equatable,並且允許使用==運算符,從而解決了各種問題,它會隱式將==運算符轉換為isEqual:調用。這樣,如果一個類實現了isEqual:,那麼我們就可以使用普通的Swift比較。比如:


let n1 = NSNumber(integer:1)
let n2 = NSNumber(integer:2)
let n3 = NSNumber(integer:3)
let ok = n2 == 2 // true 1
let ok2 = n2 == NSNumber(integer:2) // true 2
let ix = [n1,n2,n3].indexOf(2) // Optional wrapping 1 3
  

上述代碼似乎做了3件不可能的事情:

1我們直接比較了Int與NSNumber,並且得到了正確的結果,就好像比較的是Int與NSNumber所包裝的那個整數一樣。

2我們直接比較了兩個NSNumber對象,並且得到了正確的結果,就好像比較的是這兩個NSNumber對像所包裝的整數一樣。

3我們將NSNumber數組看作Equatables數組,並調用了indexOf方法,最後成功找出「等於」那個實際值的NSNumber對象。

這種魔法分為兩塊:

·數字被包裝到了NSNumber對像中。

·==運算符(背後也被indexOf方法所用)會被轉換為isEqual:調用。

NSNumber實現了isEqual:來比較兩個NSNumber對象,這是通過比較所包裝的數值來實現的;因此,相等比較可以正常使用。

如果NSObject子類沒有實現isEqual:,那麼它會繼承NSObject的實現,比較兩個對象的相等性(就像Swift的===運算符一樣)。比如,兩個Dog對象可以通過==運算符進行比較,雖然Dog並未使用Equatable也是可以的,因為它們都繼承自NSObject,但Dog沒有實現isEqual:,因此==默認將會使用NSObject的相等比較:


class Dog : NSObject {
    var name : String
    init(_ name:String) {self.name = name}
}
let d1 = Dog("Fido")
let d2 = Dog("Fido")
let ok = d1 == d2 // false
  

很多實現了isEqual:的類還實現了更為具體和高效的測試。對於Objective-C,判斷兩個NSNumber對象是否相等(即包裝了相同的數字)的常見做法是調用isEqualToNumber:。與之類似,NSString有isEqualToString:、NSDate有isEqualToDate:,諸如此類。不過,這些類還實現了isEqual:,因此我覺得最好的方式還是使用Swift==運算符。

與之類似,在Objective-C中,提供排序比較方法是每個類的職責。標準方法是compare:,它會返回3個NSComparisonResult case之一:

.OrderedAscending

接收者小於參數。

.OrderedSame

接收者等於參數。

.OrderedDescending

接收者大於參數。

Swift比較運算符(<之類的)並不會神奇地調用compare:。你不能直接比較兩個NSNumber值:


let n1 = NSNumber(integer:1)
let n2 = NSNumber(integer:2)
let ok = n1 < n2 // compile error
  

你常常需要自己調用compare:,就像在Objective-C中所做的那樣:


let n1 = NSNumber(integer:1)
let n2 = NSNumber(integer:2)
let ok = n1.compare(n2) == .OrderedAscending // true
  

10.4.8  NSIndexSet

NSIndexSet表示不重複的數字集合;其目的在於表示出有序的集合元素數字,如NSArray。這樣,比如,我們要從數組中同時獲取多個對象,那麼你就需要將所需要的索引指定為NSIndexSet。它還可以用在類似於數組的結構中;比如,可以向UITableView傳遞一個NSIndexSet來指定要插入或刪除哪個部分。

來看個具體的示例。假設一個NSArray中包含了元素1、2、3、4、8、9與10。NSIndexSet以一種更加簡潔的實現來表達這個概念,並且很容易查詢。真正的實現我們是看不到的,不過你可以認為這個NSIndexSet包含了兩個NSRange結構體:{1,4}與{8,3},NSIndexSet的方法實際上會讓你覺得一個NSIndexSet是由多個範圍構成的。

NSIndexSet是不可變的;其可變的子類是NSMutableIndexSet。你可以通過向indexSetWithIndexesInRange:傳遞一個NSRange來直接構造只有一個連續範圍的NSIndexSet;但要想構造更加複雜的索引集合,你就需要使用NSMutableIndexSet,這樣就可以附加更多的範圍了。


let arr = ["zero", "one", "two", "three", "four", "five",
    "six", "seven", "eight", "nine", "ten"]
let ixs = NSMutableIndexSet
ixs.addIndexesInRange(NSRange(1...4))
ixs.addIndexesInRange(NSRange(8...10))
let arr2 = (arr as NSArray).objectsAtIndexes(ixs)
  

可以通過for...in來遍歷(枚舉)NSIndexSet所指定的索引值;此外,還可以通過調用enumerateIndexesUsingBlock:、enumerateRangesUsingBlock:等方法來遍歷NSIndexSet的索引或範圍。

10.4.9  NSArray與NSMutableArray

NSArray是Objective-C的數組對像類型。基本上,它相當於Swift Array,並且可以彼此橋接。不過,NSArray的元素必須是對像(類與類的實例),這些對象的類型可以不同。要想完整理解Swift Array與Objective-C NSArray之間的隱式橋接與類型轉換,請參見4.12.1的「Swift Array與Objective-C NSArray」部分。

在iOS 9中,如果NSArray對像只有一種元素類型,那麼Objective-C就可以在其聲明中標記出其類型。Swift 2.0可以讀取該標記。這意味著你不會再像過去那樣接收到[AnyObject]了(不得不向下類型轉換為真正的類型)。這一點對於NSSet及NSDictionary來說也是一樣的。

NSArray的長度是其count,可以通過objectAtIndex:根據索引號獲得特定的對象。與Swift Array一樣,第一個對象的索引是0,因此最後一個對象的索引是其count減1。

相對於調用objectAtIndex:,你可以對NSArray使用下標。這並非因為NSArray會橋接到Swift Array,而是NSArray實現了objectAtIndexedSubscript:。該方法是Swift subscript getter在Objective-C中的對應之物,Swift知道這一點。實際上,當NSArray頭文件轉換為Swift時,該方法會表示為一個subscript聲明!這樣,該頭文件的Objective-C版本聲明如下所示:


- (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx;
  

不過,相同頭文件的Swift版本則如下所示:


subscript (idx: Int) -> AnyObject { get }
  

(要想理解Objective-C聲明中ObjectType的含義,請參見附錄A。)

可以通過indexOfObject:或indexOfObjectIdenticalTo:查找數組中的某個對象;前者會調用isEqual:,後者則會使用對像同一性(類似於Swift中的===)。如前所述,如果在數組中找不到對象,那麼結果就是NSNotFound。

與Swift Array不同,但類似於Objective-C NSString,NSArray是不可變的。這並不意味著你無法修改其所包含的任何對像;相反,這表示一旦構建好了NSArray,你就不能再從中刪除對像、向其插入對象,或替換掉指定索引處的對象。要想在Objective-C中完成這些事情,你可以創建一個新數組,裡面包含著原來的數組元素再加上或減去一些對象,或使用NSArray的子類NSMutableArray。Swift Array並未橋接到NSMutableArray;如果需要NSMutableArray,那就得創建它。最簡單的方式是使用NSMutableArray的初始化器init()或是init(array:)。

有了NSMutableArray後,你就可以調用NSMutableArray的addObject:及replaceOb-jectAtIndex:withObject:之類的方法了;還可以通過下標給NSMutableArray賦值。這是因為NSMutableArray實現了一個特殊的方法setObject:atIndexedSubscript:,Swift將其看作subscript setter。

此外,除了[AnyObject],你無法直接將任何類型的NSMutableArray轉換為Swift Array;通常的做法是從NSMutableArray向上類型轉換為NSArray,然後再向下類型轉換為特定類型的Swift Array:


let marr = NSMutableArray
marr.addObject(1) // an NSNumber
marr.addObject(2) // an NSNumber
let arr = marr as NSArray as! [Int]
  

Cocoa提供了通過塊來搜索或過濾數組的方式。還可以使用排序數組,並通過各種方式提供排序規則;如果是可變數組,那麼還可以直接對其排序。你可能更希望在Swift Array中執行這些操作,不過瞭解如何通過Cocoa的方式做到這一點也是很有意義的。比如:


let pep = ["Manny", "Moe", "Jack"] as NSArray
let ems = pep.objectsAtIndexes(
    pep.indexesOfObjectsPassingTest {
        obj, idx, stop in
        return (obj as! NSString).rangeOfString(
                "m", options:.CaseInsensitiveSearch
            ).location == 0
    }
) // ["Manny", "Moe"]
  

10.4.10  NSDictionary與NSMutableDictionary

NSDictionary是Objective-C的字典對像類型。它基本上類似於Swift Dictionary,並且二者之間會彼此橋接。不過,NSDictionary的鍵值必須是對像(類與類的實例),這些對象的類型可以不同;鍵必須要遵循NSCopying,並且是可以散列的。請參見4.12.2節的「Swift Dictionary與Objective-C NSDictionary」部分瞭解關於如何橋接Swift Dictionary與Objective-C NSDictionary以及類型轉換的詳細信息。

NSDictionary是不可變的;其可變子類是NSMutableDictionary。Swift Dictionary並沒有橋接到NSMutableDictionary;你可以通過初始化器init()或init(dictionary:)方便地創建一個NSMutableDictionary。

NSDictionary的鍵是不同的(使用isEqual:進行比較)。如果向NSMutableDictionary添加一個鍵值對,鍵要是不在其中,那麼這個鍵值對就會被添加進去;但如果鍵已經存在了,那麼相應的值就會被替換掉。這與Swift Dictionary的行為類似。

NSDictionary的基本用法是通過鍵來獲取一個條目的值(使用objectForKey:);如果鍵不存在,那麼結果就是nil。在Objective-C中,nil並非對象,因此它不可能是NSDictionary中的值;這個結果的含義是非常明確的。Swift通過將objectForKey:的結果看作AnyObject?來解決這一問題,即包裝了AnyObject的Optional。

我們也可以對NSDictionary和NSMutableDictionary使用下標,原因與可以對NSArray和NSMutableArray使用下標一樣。NSDictionary實現了objectForKeyedSubscript:,Swift將其看作subscript getter。此外,NSMutableDictionary實現了setObject:for-KeyedSubscript:,Swift將其看作subscript setter。

可以從NSDictionary獲取鍵的列表(allKeys)、值的列表(allValues),以及根據值排序的鍵的列表。還可以通過塊來遍歷鍵值對,甚至可以通過比較值來過濾NSDictionary。

10.4.11  NSSet及相關類

NSSet是個由不同對像構成的無序集合。「不同」意味著在使用isEqual:比較集合中的兩個對像時不會返回true。判斷集合中是否存在某個對象要比在數組中搜索高效得多,你可以判斷某個集合是否是另一個集合的子集或兩個集合是否相交。你可以使用for...in結構遍歷(枚舉)集合,當然,順序是不確定的。你可以過濾集合,就像過濾NSArray一樣。實際上,你對集合所能進行的操作類似於數組,當然,你不能對集合執行任何涉及排序含義的操作。

要想擺脫這個限制,可以使用有序集合。有序集合(NSOrderedSet)非常類似於數組,並且操作有序集合的方法也非常類似於數組的,你甚至可以通過下標獲取元素(因為實現了objectAtIndexedSubscript:)。不過,有序集合的元素必須是不同的。有序集合提供了很多優勢:比如,與NSSet一樣,判斷一個對象是否位於有序集合中要比判斷數組高效得多,你可以對集合進行並集、交集與差集等運算。既然要求元素不同,這個約束基本上算不上什麼約束(因為元素無論如何也得是不同的),因此請盡量使用NSOrderedSet而非NSArray。

將數組傳遞給有序集合會去重,這意味著順序不會發生變化,但只有相同對象的第1個才會被添加到集合中。

NSSet是不可變的。你可以通過添加或刪除元素從另一個NSSet生成一個新的,還可以使用其子類NSMutableSet。與之類似,NSOrderedSet也有其可變版本NSMutableOrderedSet(可以通過下標插入,因為它實現了setObject:atIndexed-Subscript:)。向集合中添加一個對像時,如果這個對象已經在集合中了,那麼是不會有什麼副作用的;結果就是什麼也不會添加進去(唯一性規則會起作用),但也不會報錯。

NSCountedSet是NSMutableSet的子類,它是個可變無序的對象集合,並且集合中的對象可以是相同的(這個概念通常也叫作Bag)。它被實現為一個集合,同時還會記錄下每個元素被添加的次數。

Swift Set會被橋接到NSSet。不過,NSSet的元素必須是對像(類與類的實例),這些對象的類型可以不同。請參見4.12.3節「Swift Set與Objective-C NSSet」部分瞭解詳情。Swift中並沒有與NSMutableSet、NSCountedSet、NSOrderedSet及NSMutableOrderedSet對應的橋接之物,不過可以通過初始化器從集合或數組輕鬆構建出來。此外,你可以將NSMutableSet或NSCountedSet向上類型轉換為NSSet,然後再向下類型轉換為Swift Set(類似於NSMutableArray)。NSOrderedSet帶有一個「門面」屬性,可以將其表示為數組或集合。不過由於其特殊的行為,你更傾向於以Objective-C的形式來使用NSCountedSet與NSOrderedSet。

10.4.12  NSNull

NSNull類什麼都不做,但卻提供了一個指向單例對象的指針NSNull()。有時,我們需要一個實際的Objective-C對象,但不能為nil,這個單例對象就表示nil。比如,不能將nil作為Objective-C集合元素值(如NSArray、NSSet及NSDictionary),因此需要使用NSNull()。

可以通過普通的相等運算符判斷一個對象是否等於NSNull(),因為它會使用NSObject的isEqual:,它進行的是同一性比較。這是個單例實例,因此可以使用同一性比較。

10.4.13 不變與可變

初學者有時難以理解Cocoa Foundation中成對出現的不變與可變類,其中父類都是不變的,子類都是可變的。這不禁令人想起Swift對於常量(let)與真正的變量(var)的區分,它們也有類似的結果。比如,NSArray是「不變的」,這與我們使用let來表示Swift Array是一個意思:你不能向該數組追加或插入元素,也不能替換或刪除該數組中的元素,不過如果數組中的元素是引用類型(當然,對於NSArray來說,其元素肯定是引用類型),那麼你可以就地修改元素。

Cocoa需要這些不變/可變類的原因在於防止非法修改。這些都是普通的類,NSArray對象是個普通的類實例——引用類型。如果類有一個NSArray屬性,並且該數組是可變的,那麼該數組就有可能在該類不知情的情況下被其他對像修改。為了防止這種情況發生,類在內部會臨時使用一個可變實例,然後將其存儲起來並提供給其他類一個不可變的實例,這樣可以保護值不被意外修改(Swift中就沒有這個問題,因為其基本的內建對像類型如String、Array與Dictionary等都是結構體,因此它們是值類型,無法就地修改;它們只能被替換,這可以通過setter觀察者進行防護或檢測)。

文檔中可能沒有明確表示出可變類已經重寫了其不可變父類的方法。比如,NSMutableArray的很多方法就沒有列在NSMutableArray類的文檔頁面中,因為它們都繼承自NSArray。當這種方法被可變子類繼承下來時,它們會被重寫以符合可變子類的需要。比如,NSArray的init(array:)會生成一個不可變數組,不過NSMutableArray的init(array:)(它甚至都沒有列在NSMutableArray的文檔頁面中,因為它繼承自NSArray)會生成一個可變數組。

這也回答了如何讓不可變數組成為可變以及如何讓可變數組成為不可變的問題。如果發送給NSArray類的init(array:)生成了一個新的不可變數組,新數組中包含了與原始數組相同的對象且順序相同,那麼發送給NSMutableArray類的相同的初始化器init(array:)就會生成一個可變數組,其中包含了與原始數組相同的對象且順序相同。因此,這個方法可以在不變與可變這兩個方向傳遞數組。還可以使用copy(生成一個不可變副本)與mutableCopy(生成一個可變副本),它們都繼承自NSObject;不過這兩個方法都不是很方便,因為它們生成的都是AnyObject,你還需要進行類型轉換。

這些不變/可變類都實現為了類簇,這意味著Cocoa會使用一個秘密類,這個類與文檔中所記錄的那個類是不同的。可以通過查看底層代碼瞭解到這一點;就拿NSStringFromClass(s.dynamicType)來說,其中s是個NSString,它會生成一個神秘值"__NSCFString"。你不應該在這個秘密類上花太多時間。隨著時間的流逝,這個類可能會發生變化,但卻不會通知你,而且與你也沒有任何關係;你永遠都不需要知道它。

10.4.14 屬性列表

屬性列表是數據的字符串(XML)表示。只有Foundation類NSString、NSData、NSArray與NSDictionary才能被轉換為屬性列表。此外,只有當NSArray與NSDictionary中的類是這些類以及NSDate與NSNumber時,它們才能被轉換為屬性列表。(如前所述,這正是你需要將UIColor轉換為NSData才能在user defaults中存儲的原因所在;user defaults就是個屬性列表。)

屬性列表的主要作用是將數據存儲為文件。它是值序列化的一種方式,即將值以一種形式存儲到磁盤中,然後還可以將這種形式的值重建。NSArray與NSDictionary提供了便捷方法writeToFile:atomically:與writeToURL:atomically:來分別根據給定的路徑名與URL生成屬性列表文件;相反,它們還提供了初始化器,可以根據給定文件的屬性列表內容來創建NSArray對象與NSDictionary對象。出於這個原因,在創建屬性列表時,你可以從這些類開始。((NSString與NSData的方法writeToFile:...與writeToURL:...只是將數據直接寫到文件中,而非屬性列表。)

當通過這種方式從屬性列表文件重建NSArray或NSDictionary對像時,集合、字符串對象與集合中的數據對象都是不可變的。如果希望它們是可變的,或是想將一個屬性列表類的實例轉換為另一個屬性列表,你需要使用NSPropertyListSerialization類。(參見Property List Programming Guide。)