讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 4.12 集合類型 >

4.12 集合類型

與大多數現代計算機語言一樣,Swift也擁有內建的集合類型,Array與Dictionary,以及第3種類型Set。Array與Dictionary是非常重要的,語言也對其提供了特殊的語法支持。同時,就像大多數Swift類型一樣,Swift為其提供的相關函數也是有限的,一些缺失的功能是通過Cocoa的NSArray與NSDictionary補充的,Array與NSArray,Dictionary與NSDictionary之間是彼此橋接的。Set集合類型則與Cocoa的NSSet橋接。

4.12.1  Array

數組(Array,是個結構體)是對像實例的一個有序集合(即數組元素),可以通過索引號進行訪問,索引號是個Int,值從0開始。因此,如果一個數組包含了4個元素,那麼第1個元素的索引就為0,最後一個元素的索引為3。Swift數組不可能是稀疏數組:如果有一個元素的索引為3,那麼肯定還會有一個元素的索引為2,以此類推。

Swift數組最顯著的特徵就是其嚴格的類型。與其他一些計算機語言不同,Swift數組的元素必須是統一的,也就是說,數組必須包含相同類型的元素。甚至連空數組都必須要有確定的元素類型,儘管此時數組中並沒有元素存在。數組本身的類型與其元素的類型是一致的。所包含的元素類型不同的數組被認為是兩個不同類型的數組:Int元素的數組與String元素的數組就是不同類型的數組。數組類型與其元素類型一樣也是多態的:如果NoisyDog是Dog的子類,那麼NoisyDog的數組就可以用在需要Dog數組的地方。如果這些讓你想起了Optional,那就對了。就像Optional一樣,Swift數組也是泛型。它被聲明為Array<Element>,其中佔位符Element是特定數組元素的類型。

一致性約束並沒有初看起來那麼苛刻。數組只能包含一種類型的元素,不過類型卻是非常靈活的。通過精心選取類型,你所創建的數組中,內部元素的類型可以是不同的。比如:

·如果Dog類有個NoisyDog子類,那麼Dog數組既可以包含Dog對象,也可以包含NoisyDog對象。

·如果Bird與Insect都使用了Flier協議,那麼Flier數組既可以包含Bird對象,也可以包含Insect對象。

·AnyObject數組可以包含任何類以及任何Swift橋接類型的實例,如Int、String和Dog等。

·類型本身也可以是不同的可能類型的載體。本章之前介紹的Error枚舉就是一個例子;其關聯值可能是Int或是String,這樣Error元素的數組就可以包含Int值與String值。

要想聲明或表示給定數組元素的狀態,你應該顯式解析出泛型佔位符;Int元素的數組就是Array<Int>。不過,Swift提供了語法糖來表示數組的元素類型,通過將方括號包圍元素類型名來表示,如[Int]。你在絕大多數時候都會使用這種語法。

字面值數組表示為一個方括號,裡面包含著用逗號分隔的元素列表(以及可選的空白字符):如[1,2,3]。空數組的字面值就是空的方括號。

數組的默認初始化器init()是通過在數組類型後面使用一對空的圓括號來調用的,它會生成該類型的一個空數組。因此,你可以像下面這樣創建一個空的Int數組:


var arr = [Int]
  

另外,如果提前已經知道了引用類型,那麼空數組就可以推斷為該類型。因此,你還可以像下面這樣創建一個Int類型的空數組:


var arr : [Int] = 
  

如果從包含元素的字面值數組開始,那麼通常無須聲明數組的類型,因為Swift會根據元素推斷出其類型。比如,Swift會將[1,2,3]推斷為Int數組。如果數組元素類型包含了類及其子類,如Dog與NoisyDog,那麼Swift會將父類推斷為數組的類型。甚至[1,"howdy"]也是合法的數組字面值;它會被推斷為NSObject數組。不過,在某些情況下,你還是需要顯式聲明數組引用的類型,同時將字面值賦給該數組:


let arr : [Flier] = [Insect, Bird]
  

數組也有一個初始化器,其參數是個序列。這意味著,如果類型是個序列,那麼你可以將其實例分割到數組元素中。比如:

·Array(1...3)會生成數組Int[1,2,3]。

·Array("hey".characters)會生成數組Character["h","e","y"]。

·Array(d)(其中d是個Dictionary)會生成d鍵值對的元組數組。

另一個數組初始化器init(count:repeatedValue:)可以使用相同的值來裝配數組。在該示例中,我創建了一個由100個Optional字符串構成的數組,每個Optional都被初始化為nil:


let strings : [String?] = Array(count:100, repeatedValue:nil)
  

這是Swift中最接近稀疏數組的數組創建方式;我們有100個槽,每個槽都可能包含或不包含一個字符串(一開始都不包含)。

1.數組轉換與類型檢測

在將一個數組類型賦值、傳遞或轉換為另一個數組類型時,你操作的實際上是數組中的每個元素。比如:


let arr : [Int?] = [1,2,3]
  

上述代碼實際上是個簡寫:將Int數組看作包裝了Int的Optional數組意味著原始數組中的每個Int都必須要包裝到Optional中。如下代碼所示:


let arr : [Int?] = [1,2,3]
print(arr) // [Optional(1), Optional(2), Optional(3)]
  

與之類似,假設有一個Dog類及其NoisyDog子類;那麼如下代碼就是合法的:


let dog1 : Dog = NoisyDog
let dog2 : Dog = NoisyDog
let arr = [dog1, dog2]
let arr2 = arr as! [NoisyDog]
  

在第3行,我們有一個Dog數組。在第4行,我們將該數組向下類型轉換為NoisyDog數組,這意味著我們將第1個數組中的每個Dog都轉換為了NoisyDog(這麼做應用並不會崩潰,因為第1個數組中的每個元素實際上都是個NoisyDog)。

你可以將數組的所有元素與is運算符進行比較來判斷數組本身。比如,考慮之前代碼中的Dog數組,可以這麼做:


if arr is [NoisyDog] { // ...
  

如果數組中的每個元素都是NoisyDog,那麼結果就為true。

與之類似,as?運算符會將數組轉換為包裝數組的Optional,如果底層的轉換無法進行,那麼結果就為nil:


let dog1 : Dog = NoisyDog
let dog2 : Dog = NoisyDog
let dog3 : Dog = Dog
let arr = [dog1, dog2]
let arr2 = arr as? [NoisyDog] // Optional wrapping an array of NoisyDog
let arr3 = [dog2, dog3]
let arr4 = arr3 as? [NoisyDog] // nil
  

對數組進行向下類型轉換與對任何值進行向下類型轉換的原因相同,這樣就可以向數組的元素發送恰當的消息了。如果NoisyDog聲明了一個Dog沒有的方法,那麼你就不能向Dog數組中的元素發送該消息。有時需要將元素向下類型轉換為NoisyDog,這樣編譯器就允許你發送該消息了。你可以向下類型轉換單個元素,也可以轉換整個數組;你要做的就是選擇一種安全並且在特定上下文中有意義的方式。

2.數組比較

數組相等與你想的是一樣的:如果兩個數組包含相同數量的元素,並且相同位置上的元素全都相等,那麼這兩個數組就是相等的:


let i1 = 1
let i2 = 2
let i3 = 3
if [1,2,3] == [i1,i2,i3] { // they are equal!
  

如果比較兩個數組,那麼這兩個數組不必非得是相同類型的,不過除非它們包含的對象都彼此相等,否則這兩個數組就不會相等。如下代碼比較了一個Dog數組和一個NoisyDog數組;它們實際上是相等的,因為它們實際上是以相同順序包含了相同的狗:


let nd1 = NoisyDog
let d1 = nd1 as Dog
let nd2 = NoisyDog
let d2 = nd2 as Dog
if [d1,d2] == [nd1,nd2] { // they are equal!
  

3.數組是值類型

由於數組是個結構體,因此它是個值類型而非引用類型。這意味著每次將數組賦給變量或作為參數傳遞給函數時,數組都會被複製。不過,我並不是說僅僅賦值或傳遞數組就是代價高昂的操作,這些複製每次都會發生。如果對數組的引用是個常量,那顯然就沒必要執行複製了;甚至從另一個數組生成新數組,或是修改數組的操作都是非常高效的。你要相信Swift的設計者肯定已經考慮過這些問題了,並且在背後會高效地實現數組。

雖然數組本身是個值類型,但其元素卻會按照元素本身的情況來對待。特別地,如果一個數組是類實例數組,將其賦給多個變量,那麼結果就是會有多個引用指向相同的實例。

4.數組下標

Array結構體實現了subscript方法,可以通過在對數組的引用後使用方括號來訪問元素。你可以在方括號中使用Int。比如,在一個包含著3個元素的數組中,如果數組是通過變量arr引用的,那麼arr[1]就可以訪問第2個元素。

還可以在方括號中使用Int的Range。比如,如果arr是個包含3個元素的數組,那麼arr[1...2]就表示第2與第3個元素。從技術上來說,如arr[1...2]之類的表達式會生成一個ArraySlice。不過,ArraySlice非常類似於數組;比如,你可以像對數組一樣對ArraySlice使用下標,在需要數組的地方也可以傳遞ArraySlice過去。一般來說,你可以認為ArraySlice就是個數組。

如果對數組的引用是可變的(var而非let),那麼就可以對下標表達式賦值。這麼做會改變槽中的值。當然,賦的值一定要與數組元素的類型保持一致:


var arr = [1,2,3]
arr[1] = 4 // arr is now [1,4,3]
  

如果下標是個範圍,那麼賦的值就必須是個數組。這會改變被賦值的數組的長度:


var arr = [1,2,3]
arr[1..<2] = [7,8] // arr is now [1,7,8,3]
arr[1..<2] =  // arr is now [1,8,3]
arr[1..<1] = [10] // arr is now [1,10,8,3] (no element was removed!)
  

如果訪問數組元素時使用的下標大於最大值或小於最小值,那就會產生運行時錯誤。如果arr有3個元素,那麼arr[-1]與arr[3]從語義上來說並沒有錯,但程序將會崩潰。

5.嵌套數組

數組元素也可以是數組,比如:


let arr = [[1,2,3], [4,5,6], [7,8,9]]
  

這是個Int數組的數組。因此,其類型聲明是[[Int]]。(被包含的數組不一定非得是相同長度的,我這麼做只是為了清晰。)

要想訪問嵌套數組中的每個Int,你可以將下標運算符鏈接起來:


let arr = [[1,2,3], [4,5,6], [7,8,9]]
let i = arr[1][1] // 5
  

如果外層數組引用是可變的,那麼你還可以對嵌套數組賦值:


var arr = [[1,2,3], [4,5,6], [7,8,9]]
arr[1][1] = 100
  

還可以通過其他方式修改內部數組;比如,可以插入新的元素。

6.基本的數組屬性與方法

數組是一個集合(CollectionType協議),集合本身又是個序列(SequenceType協議)。你可能對此有所瞭解:String的characters就是這樣的,我在第3章將其稱作字符序列。出於這個原因,數組與字符序列是非常相似的。

作為集合,數組的count是個只讀屬性,返回數組中的元素個數。如果數組的count為0,那麼其isEmpty屬性就為true。

數組的first與last只讀屬性會返回其第一個和最後一個元素,不過這些元素會被包裝到Optional中,因為數組可能為空,因此這些屬性就會為nil。這會出現Swift中很少會遇到的將一個Optional包裝到另一個Optional中的情況。比如,考慮包裝Int的Optional數組,如果獲取該數組最後一個屬性會發生什麼。

數組最大的可訪問索引要比其count小1。你常常會使用對count的引用來計算索引值;比如,要想引用arr的最後兩個元素,可以這樣做:


let arr = [1,2,3]
let arr2 = arr[arr.count-2...arr.count-1] // [2,3]
  

Swift並未使用現在普遍採用的通過負數來計算索引這一約定。另一方面,如果想要訪問數組的最後n個元素,你可以使用suffix方法:


let arr = [1,2,3]
let arr2 = arr.suffix(2) // [2,3]
  

suffix與prefix有一個值得注意的特性,那就是超出範圍後並不會出錯:


let arr = [1,2,3]
let arr2 = arr.suffix(10) // [1,2,3] (and no crash)
  

相對於通過數量來描述後綴或前綴的大小,你可以通過索引來表示後綴或前綴的限制:


let arr = [1,2,3]
let arr2 = arr.suffixFrom(1) // [2,3]
let arr3 = arr.prefixUpTo(1) // [1]
let arr4 = arr.prefixThrough(1) // [1,2]
  

數組的startIndex屬性值是0,其endIndex屬性值是其count。此外,數組的indices屬性值是個半開區間,其端點是startIndex與endIndex,即訪問整個數組的範圍。如果通過可變引用來訪問這個範圍,那就可以修改其startIndex與endIndex來生成一個新的範圍。第3章對字符序列就是這麼做的;不過,數組的索引值是Int,因此你可以使用普通的算術運算符:


let arr = [1,2,3]
var r = arr.indices
r.startIndex = r.endIndex-2
arr2 = arr[r] // [2,3]
  

indexOf方法會返回一個元素在數組中首次出現位置處的索引,不過它被包裝到了Optional中,這樣如果數組中不存在這個元素就會返回nil。如果數組包含了Equatables,那比較時就可以通過==來識別待尋找的元素:


let arr = [1,2,3]
let ix = arr.indexOf(2) // Optional wrapping 1
  

即便數組沒有包含Equatables,你也可以提供自定義的函數,它接收一個元素類型並返回一個Bool,你會得到返回true的第一個元素。在如下示例中,Bird結構體有一個name String屬性:


let aviary = [Bird(name:"Tweety"), Bird(name:"Flappy"), Bird(name:"Lady")]
let ix = aviary.indexOf {$0.name.characters.count < 5} // Optional(2)
  

作為序列,數組的contains方法會判斷它是否包含了某個元素。如果元素是Equatables,那麼你依然可以使用==運算符;也可以提供自定義函數,該函數接收一個元素類型並返回一個Bool:


let arr = [1,2,3]
let ok = arr.contains(2) // true
let ok2 = arr.contains {$0 > 3} // false
  

startsWith方法判斷數組的起始元素是否與給定的相同類型序列的元素相匹配。這裡依然可以使用==運算符來比較Equatables;你也可以提供自定義函數,該函數接收兩個元素類型值,並返回表示它們是否匹配的Bool:


let arr = [1,2,3]
let ok = startsWith(arr, [1,2]) // true
let ok2 = arr.startsWith([1,-2]) {abs($0) == abs($1)} // true
  

elementsEqual方法是數組比較的序列泛化:兩個序列長度必須相同,其元素要麼是Equatables,要麼你自己提供匹配函數。

minElement與maxElement方法分別返回數組中最小與最大的元素,並且被包裝到Optional中,目的是防止數組為空。如果數組包含了Comparables,那麼你可以使用<運算符;此外,你可以提供一個返回Bool的函數,表示兩個給定元素中較小的那個是不是第一個:


let arr = [3,1,-2]
let min = arr.minElement // Optional(-2)
let min2 = arr.minElement {abs($0)<abs($1)} // Optional(1)
  

如果對數組的引用是可變的,那麼append與appendContentsOf實例方法就會將元素添加到數組末尾。這兩個方法的差別在於append會接收單個元素類型值,而appendContentsOf則接收元素類型的一個序列。比如:


var arr = [1,2,3]
arr.append(4)
arr.appendContentsOf([5,6])
arr.appendContentsOf(7...8) // arr is now [1,2,3,4,5,6,7,8]
  

若+運算符左側是個數組,那麼+就會被重載,其行為類似於appendContentsOf(而非append!),只不過它會生成一個新數組,因此即便對數組的引用是個常量,這麼做也是可行的。如果對數組的引用是可變的,那麼你可以通過+=運算符對其進行擴展。比如:


let arr = [1,2,3]
let arr2 = arr + [4] // arr2 is now [1,2,3,4]
var arr3 = [1,2,3]
arr3 += [4] // arr3 is now [1,2,3,4]
  

如果對數組的引用是可變的,那麼實例方法insert(atIndex:)就會在指定的索引處插入一個元素。要想同時插入多個元素,請使用範圍下標數組進行賦值,就像之前介紹的那樣(此外,還有一個insertContentsOf(at:)方法)。

如果對數組的引用是可變的,那麼實例方法removeAtIndex會刪除指定索引處的元素;實例方法removeLast會刪除最後一個元素,removeFirst則會刪除第一個元素。這些方法還會返回從數組中刪除的值;如果不需要,那麼可以忽略返回值。這些方法不會將返回值包裝到Optional中,訪問越界的索引會導致程序崩潰。另一種形式的removeFirst可以指定刪除的元素數量,不過它不返回值;如果數組中沒有那麼多元素,那麼程序將會崩潰。

另外,popFirst與popLast則會展開Optional中的返回值;這樣,即便數組為空也是安全的。如果引用不可變,那麼你可以通過dropFirst與dropLast方法返回刪除了最後一個元素後的數組(實際上是個切片)。

joinWithSeparator實例方法接收一個數組的數組。它會提取出每個元素,並將參數數組中的元素插入到提取出的每個元素序列之間。結果是個叫作JoinSequence的中間序列;如果必要,還需要將其轉換為Array。比如:


let arr = [[1,2], [3,4], [5,6]]
let arr2 = Array(arr.joinWithSeparator([10,11]))
// [1, 2, 10, 11, 3, 4, 10, 11, 5, 6]
  

調用joinWithSeparator時將空數組作為參數可以將數組的數組打平:


let arr = [[1,2], [3,4], [5,6]]
let arr2 = Array(arr.joinWithSeparator())
// [1, 2, 3, 4, 5, 6]
  

還有一個flatten實例方法也可以做到這一點。它會返回一個中間序列(或集合),你可能需要將其轉換為Array:


let arr = [[1,2], [3,4], [5,6]]
let arr2 = Array(arr.flatten)
// [1, 2, 3, 4, 5, 6]
  

reverse實例方法會生成一個新數組,其元素順序與原始數組的相反。

sortInPlace實例方法會對原始數組排序(如果對數組的引用是可變的),而sort實例方法則會根據原始數組生成一個新數組。你有兩個選擇:如果是Comparables數組,那就可以通過<運算符指定新順序;此外,你可以提供一個函數,該函數接收兩個元素類型參數並返回一個Bool,表示第1個參數是否應該位於第2個參數之前(就像minElement與maxElement一樣)。比如:


var arr = [4,3,5,2,6,1]
arr.sortInPlace // [1, 2, 3, 4, 5, 6]
arr.sortInPlace {$0 > $1} // [6, 5, 4, 3, 2, 1]
  

在最後一行代碼中,我提供了一個匿名函數。當然,你還可以將一個聲明好的函數名作為參數傳遞進來。在Swift中,比較運算符其實就是函數名。因此,我能以更加簡潔的方式完成相同的功能,比如:


var arr = [4,3,5,2,6,1]
arr.sortInPlace(>) // [6, 5, 4, 3, 2, 1]
  

split實例方法會在通過測試的元素位置處將一個數組分解為數組的數組,這裡的測試指的是一個函數,它接收一個元素類型值並返回Bool;通過測試的元素會被去除:


let arr = [1,2,3,4,5,6]
let arr2 = arr.split {$0 % 2 == 0} // split at evens: [[1], [3], [5]]
  

7.數組枚舉與轉換

數組是一個序列,因此你可以對其進行枚舉,並按照順序查看或操縱每個元素。最簡單的方式是使用for...in循環;第5章將會對此進行更為詳盡的介紹:


let pepboys = ["Manny", "Moe", "Jack"]
for pepboy in pepboys {
    print(pepboy) // prints Manny, then Moe, then Jack
}
  

此外,你還可以使用forEach實例方法。其參數是個函數,該函數接收數組(或是其他序列)中的一個元素並且沒有返回值。你可以將其看作命令式for...in循環的函數式版本:


let pepboys = ["Manny", "Moe", "Jack"]
pepboys.forEach {print($0)} // prints Manny, then Moe, then Jack
  

如果既需要索引號又需要元素,那麼請調用enumerate實例方法並對結果進行循環;每次迭代得到的將是一個元組:


let pepboys = ["Manny", "Moe", "Jack"]
for (ix,pepboy) in pepboys.enumerate {
    print("Pep boy \(ix) is \(pepboy)") // Pep boy 0 is Manny, etc.
}
// or:
pepboys.enumerate.forEach {print("Pep boy \($0.0) is \($0.1)")}
  

Swift還提供了3個強大的數組轉換實例方法。就像forEach一樣,這些方法都會枚舉數組,這樣循環就被隱藏到了方法調用中,代碼也變得更加緊湊和整潔。

首先來看看map實例方法。它會生成一個新數組,數組中的每個元素都是將原有數組中相應元素傳遞給你所提供的函數進行處理後的結果。該函數接收一個元素類型的參數,並返回可能是其他類型的結果;Swift通常會根據函數返回的類型推斷出生成的數組元素的類型。

比如,如下代碼演示了如何將數組中的每個元素乘以2:


let arr = [1,2,3]
let arr2 = arr.map {$0 * 2} // [2,4,6]
  

如下示例演示了map實際上可以生成不同元素類型的新數組:


let arr = [1,2,3]
let arr2 = arr.map {Double($0)} // [1.0, 2.0, 3.0]
  

下面是個實際的示例,展示了使用map後代碼將會變得多麼簡潔和緊湊。為了刪除UITableView中一個段中的所有單元格,我需要將單元格定義為NSIndexPath對象的數組。如果sec是段號,那麼我就可以像下面這樣分別構建這些NSIndexPath對像:


let path0 = NSIndexPath(forRow:0, inSection:sec)
let path1 = NSIndexPath(forRow:1, inSection:sec)
// ...
  

這裡有個規律!我可以通過for...in循環行值來生成NSIndexPath對象的數組。不過,要是使用map,那麼表示相同的循環就會變得更加緊湊(ct是段中的行數):


let paths = Array(0..<ct).map {NSIndexPath(forRow:$0, inSection:sec)}
  

實際上,map是CollectionType的一個實例方法,Range本身是個CollectionType。因此,我不需要轉換為數組:


let paths = (0..<ct).map {NSIndexPath(forRow:$0, inSection:sec)}
  

filter實例方法也會生成一個新數組。新數組中的每個元素都是老數組中的,順序也相同;不過,老數組中的一些元素可能會被去除,它們被過濾掉了。起過濾作用的是你所提供的函數;它接收一個元素類型的參數並返回一個Bool,表示這個元素是否應該被放到新數組中。

比如:


let pepboys = ["Manny", "Moe", "Jack"]
let pepboys2 = pepboys.filter{$0.hasPrefix("M")} // [Manny, Moe]
  

最後來看看reduce實例方法。如果學過LISP或Scheme,那麼你對reduce就會很熟悉;否則,初學起來會覺得很困惑。這是一種將數組中(實際上是序列)所有元素合併為單個值的方式。這個值的類型(結果類型)不必與數組元素類型相同。你提供了一個函數,它接收兩個參數;第1個是結果類型,第2個是元素類型,結果是這兩個參數的組合,它們作為結果類型。每次迭代的結果會作為下一次迭代的第一個參數,同時數組的下一個元素會作為第二個參數。因此,組合對不斷累積的輸出,以及最終的累積值就是reduce函數最終的輸出。不過,這並沒有說明第一次迭代的第一個參數來自哪裡。答案就是你需要自己提供reduce調用的第一個參數。

通過一個簡單的示例說明會加強理解。假設有一個Int數組,接下來我們可以通過reduce得到數組中所有元素的和。如下偽代碼省略了reduce調用的第一個參數,這樣你就可以思考它應該是什麼了:


let sum = arr.reduce(/*???*/) {$0 + $1}
  

每一對參數都會一起添加進去,從而得到下一次迭代時的第一個參數。每次迭代時的第2個參數都是數組中的元素。那麼問題來了,數組的第一個元素會與什麼相加呢?我們想要得到所有元素的和,既不多也不少;顯然,數組的第一個元素應該與0相加!下面是具體代碼:


let arr = [1, 4, 9, 13, 112]
let sum = arr.reduce(0) {$0 + $1} // 139
  

上述代碼可以更加簡潔一些,因為+運算符是所需類型的函數名:


let sum = arr.reduce(0, combine:+)
  

在我的iOS編程生涯中,我大量使用了這些方法,通常使用其中2個,或3個都用,將它們嵌套起來、鏈接起來,或二者結合起來使用。下面來看個示例;這個示例很複雜,但卻能非常好地展現出通過Swift來處理數組是多麼簡潔。我有一個表視圖,它將數據以段的形式展現出來。在底層,數據是個String數組的數組,每個子數組都表示段的行。現在,我想要過濾該數據,去除不包含某個子字符串的所有字符串。我想要保持段的完整性,不過如果刪除字符串導致一個段的所有字符串都被刪除,那麼我需要刪除整個段數組。

其核心是判斷一個字符串是否包含了子字符串。這裡使用的是Cocoa方法,因為可以通過它們執行不區分大小寫的搜索。如果s是數組中的字符串,並且target是我們要搜索的子字符串,那麼判斷s是否不區分大小寫地包含了target的代碼如下所示:


let options = NSStringCompareOptions.CaseInsensitiveSearch
let found = s.rangeOfString(target, options: options)
  

回憶一下第3章介紹的rangeOfString。如果found不為nil,那就說明找到了子字符串。下面是具體代碼,前面加上了一些示例數據:


let arr = [["Manny", "Moe", "Jack"], ["Harpo", "Chico", "Groucho"]]
let target = "m"
let arr2 = arr.map {
    $0.filter {
        let options = NSStringCompareOptions.CaseInsensitiveSearch
        let found = $0.rangeOfString(target, options: options)
        return (found != nil)
    }
}.filter {$0.count > 0}
  

前兩行代碼設定了示例數據,剩下的是一條命令:一個map調用,其函數包含了一個filter調用,後面又鏈接了一個filter調用。如果上述代碼都說明不了Swift有多麼酷,那就沒別的能夠說明了。

8.Swift Array與Objective-C NSArray

在編寫iOS應用時,你會導入Foundation框架(或是UIKit,因為它會導入Foundation),它包含了Objective-C NSArray類型。Swift的Array類型與Objective-C的NSArray類型是橋接的;不過,前提是數組中的元素類型可以橋接。相比於Swift,Objective-C對於NSArray中可以放置什麼元素的規則既寬鬆又嚴格。一方面,NSArray中的元素不必是相同類型的。另一方面,NSArray中的元素必須是對象,因為只有對象才能為Objective-C所理解。一般來說,如果類型能夠向上轉換為AnyObject(這意味著它是個類類型),或是如Int、Double及String這樣特殊的橋接結構體,那麼它才可以橋接到Objective-C。

將Swift數組傳遞給Objective-C通常是很簡單的。如果Swift數組包含了可以向上類型轉換為AnyObject的對象,那麼直接傳遞數組即可;要麼通過賦值,要麼作為函數調用的實參:


let arr = [UIBarButtonItem, UIBarButtonItem]
self.navigationItem.leftBarButtonItems = arr
self.navigationItem.setLeftBarButtonItems(arr, animated: true)
  

要想在Swift數組上調用NSArray方法,你需要將其轉換為NSArray:


let arr = ["Manny", "Moe", "Jack"]
let s = (arr as NSArray).componentsJoinedByString(", ")
// s is "Manny, Moe, Jack"
  

Swift數組可以看出var引用是可變的,不過無論怎麼看,NSArray都是不可變的。要想在Objective-C中獲得可變數組,你需要NSArray的子類NSMutableArray。你不能將Swift數組通過類型轉換、賦值或傳遞的方式賦給NSMutableArray,必須要強制進行。最佳方式是調用NSMutableArray的初始化器init(array:),你可以直接向其傳遞一個Swift數組:


let arr = ["Manny", "Moe", "Jack"]
let arr2 = NSMutableArray(array:arr)
arr2.removeObject("Moe")
  

將NSMutableArray轉換回Swift數組只需直接轉換即可;如果需要一個擁有原始Swift類型的數組,那就需要轉換兩次才能編譯通過:


var arr = ["Manny", "Moe", "Jack"]
let arr2 = NSMutableArray(array:arr)
arr2.removeObject("Moe")
arr = arr2 as NSArray as! [String]
  

如果Swift對像類型不能向上轉換為AnyObject,那麼它就無法橋接到Objective-C;如果需要一個NSArray,但你傳遞了一個包含這種類型的Array,那麼編譯器就會報錯。在這種情況下,你需要手工「橋接」數組元素。

比如,我有一個Swift的CGPoint數組。這在Swift中是沒問題的,不過由於CGPoint是個結構體,而Objective-C並不會將其視為一個對象,因此你不能將其放到NSArray中。如果在需要NSArray的地方傳遞了這個數組,那就會導致編譯錯誤:「[CGPoint]is not convertible to NSArray.」。解決辦法就是將每個CGPoint包裝為NSValue,這是個Objective-C對像類型,專門用作各種非對像類型的載體;現在,我們有了一個Swift的NSValue數組,接下來就可以由Objective-C進行處理了:


let arrNSValues = arrCGPoints.map { NSValue(CGPoint:$0) }
  

另一種情況是Swift的Optional數組。Objective-C集合不能包含nil(因為在Objective-C中,nil不是對像)。因此,你不能將Optional放到NSArray中。在需要NSArray時如果傳遞Optional數組,那就需要事先對這些Optional進行處理。如果Optional包裝了值,那麼你可以將其展開。不過,如果Optional沒有包裝值(它是個nil),那麼就無法將其展開。一種解決辦法就是採取Objective-C中的做法。Objective-C NSArray不能包含nil,因此Cocoa提供了一個特殊的類NSNull,當需要一個對像時,其單例NSNull()可以代替nil。這樣,如果有一個包裝了String的Optional數組,那麼我可以將那些不為nil的元素展開,並使用NSNull()替代nil元素:


let arr2 : [AnyObject] =
    arr.map{if $0 == nil {return NSNull} else {return $0!}}
  

(第5章將會進一步簡化上述代碼。)

現在來看看將NSArray從Objective-C傳遞給Swift時會發生什麼。跨越橋接不會有任何問題:NSArray會安全地變成Swift Array。不過,這是個什麼類型的Swift Array呢?就其本身來說,NSArray並沒有攜帶關於它所包含的元素類型的任何信息。因此,默認就是Objective-C NSArray會轉換為Swift的AnyObject數組。

幸好,現在不會像之前那樣再遇到這種默認情況了。從Xcode 7開始,Objective-C語言發生了變化,NSArray、NSDictionary與NSSet的聲明(這3種集合類型會橋接到Swift)已經包含了元素類型信息(Objective-C稱為輕量級泛型)。在iOS 9中,Cocoa API也得到了改進,它們包含了這些信息。這樣,在大多數情況下,從Cocoa接收到的數組都是帶有類型的。

比如,如下優雅的代碼在之前是不可能的:


let arr = UIFont.familyNames.map {
    UIFont.fontNamesForFamilyName($0)
}
  

結果是一個String數組的數組,根據字體系列列出了所有可用的字體。上述代碼可以編譯通過,因為Swift會看到UIFont的這兩個類方法返回了一個String數組。之前,這兩個數組是沒有類型信息的(它們都是AnyObject數組),你需要將其向下類型轉換為String數組。

雖然不常見,但你還是有可能從Objective-C接收到AnyObject數組。如果出現了這種情況,那麼通常你會將其進行向下類型轉換,或是將其轉換為特定Swift類型的數組。如下Objective-C類包含了一個方法,其NSArray返回類型不帶元素類型:


@implementation Pep
- (NSArray*) boys {
    return @[@"Mannie", @"Moe", @"Jack"];
}
@end
  

要想調用該方法並對結果進行處理,你需要將結果向下類型轉換為String數組。如果確信這一點,那就可以執行強制類型轉換:


let p = Pep
let boys = p.boys as! [String]
  

不過,與任何類型轉換一樣,請確保你的轉換是正確的!Objective-C數組可以包含多種對像類型。不要將這樣的數組強制向下類型轉換為並非每個元素都能轉換的類型,否則一旦轉換失敗,程序將會崩潰;在排除或轉換有問題的元素時要深思熟慮。

4.12.2  Dictionary

字典(Dictionary,是個結構體)是成對對象的一個無序集合。對於每一對對像來說,第1個對象是鍵,第2個對象是值。其用法是通過鍵來訪問值。鍵通常是字符串,不過並不局限為字符串;形式上的要求是鍵的類型要使用Hashable協議,這意味著它們使用了Equatable,並且有一個hashValue屬性(一個Int),這樣兩個相等的鍵就會擁有相等的散列值,而兩個不相等的鍵的散列值也不等。因此,背後可以通過散列值實現鍵的快速訪問。Swift的數字類型、字符串與枚舉都是Hashable。

就像數組一樣,給定的字典類型必須是統一的。鍵類型與值類型不必是相同類型,通常情況下其類型也不相同。不過在任何字典中,所有鍵的類型都必須是相同的,所有值的類型也必須是相同的。字典其實是個泛型,其佔位符類型先是鍵類型,然後是值類型:Dictionary<Key,Value>。不過與數組一樣,Swift為字典類型的表示提供了語法糖,通常情況下都會這麼用:[Key:Value],即方括號中包含了一個冒號(以及可選的空格),兩邊是鍵類型與值類型。如下代碼創建了一個空的字典,其鍵(如果存在)是String,值(如果存在)也是String:


var d = [String:String]
  

冒號還用於字典字面值語法中,用於分隔每一對鍵值。鍵值對位於方括號中,中間用逗號分隔,就像數組一樣。如下代碼通過字面值方式創建了一個字典(字典的類型[String:String]會被推斷出來):


var d = ["CA": "California", "NY": "New York"]
  

空字典的字面值是個裡面只包含了一個冒號[:]的方括號。如果通過其他方式獲悉了字典的類型,那就可以使用這個符號表示。下面是創建空字典([String:String])的另一種方式:


var d : [String:String] = [:]
  

如果通過不存在的鍵獲取值,那麼不會出現錯誤,不過Swift需要通過一種方式告知你這個操作失敗了;因此,它會返回nil。這反過來又會表示,如果成功通過一個鍵訪問到了值,那麼返回的值一定是個包裝真實值的Optional!

我們常常通過下標訪問字典的內容。要想根據鍵獲取其值,請對字典引用應用下標,下標中是鍵:


let d = ["CA": "California", "NY": "New York"]
let state = d["CA"]
  

不過請記住,在上述代碼執行後,state並不是String,它是個包裝了String的Optional!忘記這一點是很多初學者常犯的錯誤。

如果對字典的引用是可變的,那麼你還可以對鍵下標表達式賦值。如果鍵已經存在,那麼其值就會被替換。如果鍵不存在,那麼它會被創建,並且將值關聯到鍵上:


var d = ["CA": "California", "NY": "New York"]
d["CA"] = "Casablanca"
d["MD"] = "Maryland"
// d is now ["MD": "Maryland", "NY": "New York", "CA": "Casablanca"]
  

還可以調用updateValue(forKey:);好處在於它會將舊值包裝到Optional中返回,如果鍵不存在則會返回nil。

作為一種便捷方式,如果鍵存在,那麼將nil賦給鍵下標表達式會將該鍵值對刪除:


var d = ["CA": "California", "NY": "New York"]
d["NY"] = nil // d is now ["CA": "California"]
  

還可以調用removeValueForKey;好處在於在刪除鍵值對之前它會返回被刪除的值。返回的被刪除的值會包裝到一個Optional中;因此,如果返回nil,那就表示這個鍵本來就不在字典中。

與數組一樣,字典類型也可以進行向下類型轉換,這意味著其中的每個元素都會進行向下類型轉換。通常只有值類型會不同:


let dog1 : Dog = NoisyDog
let dog2 : Dog = NoisyDog
let d = ["fido": dog1, "rover": dog2]
let d2 = d as! [String : NoisyDog]
  

與數組一樣,is可用於測試字典中的實際類型,as?可用於測試並安全地進行類型轉換。與數組相等性一樣,字典相等性的工作方式與你想的是一樣的。

1.基本的字典屬性與枚舉

字典有個count屬性,它會返回字典中所包含的鍵值對數量;還有一個isEmpty屬性,用於判斷這個數量是否為0。

字典有個keys屬性,它會返回字典中所有的鍵;還有一個values屬性,它會返回字典中所有的值。它們都是沒有對外公開的結構體(實際類型是LazyForwardCollection),不過在通過for...in枚舉它們時,你會得到期望的類型:


var d = ["CA": "California", "NY": "New York"]
for s in d.keys {
    print(s) // s is a String
}   

字典是無序的!你可以枚舉它(鍵或值),但不要期望元素會以任何特定的順序返回。

可以通過將keys屬性或values屬性轉換為數組一次性獲得字典的鍵或值:


var d = ["CA": "California", "NY": "New York"]
var keys = Array(d.keys)
  

還可以枚舉字典本身。你可能已經想到了,每次迭代都會得到一個鍵值對元組:


var d = ["CA": "California", "NY": "New York"]
for (abbrev, state) in d {
    print("\(abbrev) stands for \(state)")
}
  

可以通過將字典轉換為數組,從而一次性以數組(鍵值對元組)的形式獲得字典的全部內容:


var d = ["CA": "California", "NY": "New York"]
let arr = Array(d) // [("NY", "New York"), ("CA", "California")]
  

就像數組一樣,字典、其keys屬性與values屬性都是集合(CollectionType)與序列(SequenceType)。因此,上面所介紹的關於作為集合與序列的數組的一切也都適用於字典!比如,如果字典d有Int值,那麼你可以通過reduce實例方法求出其和:


let sum = d.values.reduce(0, combine:+)
  

可以獲取其最小值(包裝在Optional中):


let min = d.values.minElement
  

可以列出符合某個標準的值:


let arr = Array(d.values.filter{$0 < 2})
  

(這裡需要轉換為Array,因為filter得到的序列是延遲的:直到枚舉它或將其放到數組中後,它裡面才會有內容)。

2.Swift Dictionary與Objective-C NSDictionary

Foundation框架中的字典類型是NSDictionary,而Swift的Dictionary類型會與其橋接。在雙方之間傳遞字典就像之前介紹的數組那樣。NSDictionary的無類型信息的橋接API的類型是[NSObject:AnyObject],它使用Objective-C Foundation對像基類作為鍵;之所以這麼做有幾個原因,不過從Swift的視角來看,主要的原因在於AnyObject並非Hashable。另一方面,NSObject被Swift API擴展了,並且使用了Hashable;由於NSObject是Cocoa類的基類,因此任何Cocoa類型都是NSObject。這樣,NSDictionary就可以橋接了。

就像NSArray一樣,NSDictionary的鍵與值類型現在可以在Objective-C中標記了。在實際的Cocoa NSDictionary中,最常使用的鍵類型是NSString,因此接收到的NSDictionary會是個[String:AnyObject]。不過,NSDictionary中擁有特定類型值的情況並不多見;傳給Cocoa或從Cocoa接收的字典通常具有不同類型的值。一個字典的鍵是字符串,但值包含了字符串、數字、顏色以及數組這一情況是非常常見的。出於這一原因,你通常不會對整個字典的類型進行向下類型轉換;相反,你在使用字典時會將值看作AnyObject,在從字典中獲取到單個值時才進行類型轉換。由於從下標鍵中返回的值本身是個Optional,所以常常需要先展開值,然後再進行類型轉換。

下面是個示例。Cocoa NSNotification對像有個userInfo屬性。它是個NSDictionary,本身可能為nil,因此Swift API是這樣描述它的:


var userInfo: [NSObject : AnyObject]? { get }
  

假設這個字典包含一個"progress"鍵,其值是個NSNumber,值裡面包含著一個Double。我的目標是將該NSNumber提取出來,並將其包含到Double賦給屬性self.progress。下面是一種安全的做法,使用可選展開與可選類型轉換(n是個NSNotification對像):


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

這是個Optional鏈,鏈的最後會獲取NSNumber的doubleValue屬性;因此,prog的隱式類型是個包裝了Double的Optional。上述代碼是安全的,因為如果沒有userInfo屬性,或字典中不包含"progress"鍵,或鍵的值不是個NSNumber,那麼什麼都不會發生,prog值就是nil。接下來判斷prog是否為nil;如果不是,那我就可以安全地強制展開它了,並且展開的值就是我所期望的Double。

(第5章將會介紹完成相同事情的另一種語法,使用了條件綁定。)

與之相反,下面這個示例會創建一個字典並將其傳遞給Cocoa。該字典是個混合體:其值包含了UIFont、UIColor及NSShadow;其鍵都是字符串,並且是從Cocoa中獲取的常量。我以字面值形式構造這個字典,然後將其傳遞過去,整個過程一步搞定,完全不需要類型轉換:


UINavigationBar.appearance.titleTextAttributes = [
    NSFontAttributeName : UIFont(name: "ChalkboardSE-Bold", size: 20)!,
    NSForegroundColorAttributeName : UIColor.darkTextColor,
    NSShadowAttributeName : {
        let shad = NSShadow
        shad.shadowOffset = CGSizeMake(1.5,1.5)
        return shad
    }
]
  

與NSArray和NSMutableArray一樣,如果希望Cocoa能夠修改字典,那就需要將其轉換為NSMutableDictionary。在如下示例中,我想要連接兩個字典,因此使用了NSMutableDictionary,它有一個addEntriesFromDictionary:方法:


var d1 = ["NY":"New York", "CA":"California"]
let d2 = ["MD":"Maryland"]
let mutd1 = NSMutableDictionary(dictionary:d1)
mutd1.addEntriesFromDictionary(d2)
d1 = mutd1 as NSDictionary as! [String:String]
// d1 is now ["MD": "Maryland", "NY": "New York", "CA": "California"]
  

這種事情經常會遇到,因為並沒有將一個字典的元素添加到另一個字典中的原生方法。實際上在Swift中,與字典相關的原生輔助方法數量是非常少的:其實根本就沒有。Cocoa與Foundation框架還可以為我們所用,也許Apple覺得沒必要在Swift標準庫中重複Foundation中已經存在的那些功能。如果覺得使用Cocoa很麻煩,那麼你可以編寫自己的庫;比如,我們可以通過擴展輕鬆將addEntriesFromDictionary:重新實現為Swift Dictionary的實例方法:


extension Dictionary {
    mutating func addEntriesFromDictionary(d:[Key:Value]) { // generic types
        for (k,v) in d {
            self[k] = v
        }
    }
}
  

4.12.3  Set

集合(Set,是個結構體)是不重複對象的一個無序集合。它非常類似於字典的鍵!其元素必須是相同類型的,它有一個count屬性和一個isEmpty屬性;可以通過任意序列進行初始化;你可以通過for...in遍歷其元素。不過,元素的順序是不確定的,你不應該假定元素的順序。

Set元素的唯一性是通過限制其類型使用Hashable協議來做到的,就像Dictionary的鍵一樣。因此,背後可以使用散列值來加速訪問。你可以通過contains實例方法判斷一個集合中是否包含了給定的元素,其效率要比對數組進行相同的操作高很多。因此,如果元素的唯一性是可以接受的(或需要這樣),並且不需要索引,也不需要確保順序,那麼相比於數組,Set會是一個更好的選擇。

集合的元素是Hashables,這意味著它們一定也都是Equatables。這是非常有意義的,因為唯一性這個概念取決於能夠回答給定對像在集合中是否已經存在這一問題。

Swift中並沒有Set字面值,你也不需要,因為在需要集合的地方可以傳遞一個數組字面值。Swift也沒有提供集合類型表示的語法糖,因為Set結構體是一個泛型,因此可以通過顯式特化泛型來表示類型信息:


let set : Set<Int> = [1, 2, 3, 4, 5]
  

不過在上述示例中,我們無須特化泛型,因為Int類型可以通過數組推斷出來。

很多時候你想要獲取到集合中的某一個元素作為樣本。由於順序是無意義的,因此獲取任意一個元素就可以,如第一個元素。如果出於這個目的,你可以使用first實例屬性;它會返回一個Optional,以防止集合為空,沒有第一個元素。

集合的標誌性特性在於其對象的唯一性。如果將對像添加到集合中,同時集合中已經包含了該對象,那麼它就不會被再次添加進去。將數組轉換為集合,然後又將集合轉換為數組是一種快速且可靠的確保數組元素唯一性的方式,不過數組元素的順序並不會保留下來:


let arr = [1,2,1,3,2,4,3,5]
let set = Set(arr)
let arr2 = Array(set) // [5,2,3,1,4], perhaps
  

Set是一種集合(CollectionType),也是個序列(SequenceType),這類似於數組與字典,之前介紹的關於這兩種類型的一切也都適用於Set。比如,Set有map實例方法;它返回一個數組,當然,如果需要也可以將其轉換回Set:


let set : Set = [1,2,3,4,5]
let set2 = Set(set.map {$0+1}) // {6, 5, 2, 3, 4}, perhaps
  

如果對集合的引用是可變的,那就有很多實例方法可供使用了。你可以通過insert向集合添加對像;如果對像已經在集合中,那就什麼都不會發生,但也沒有問題。可以通過remove方法從集合中刪除指定對象並返回;它會返回包裝在Optional中的對象,如果對像不存在,那麼該方法會返回nil。可以通過removeFirst方法刪除並返回集合中的第1個對象(無論第1個指的是什麼);如果集合為空,那麼應用就會崩潰,因此請小心行事(或使用安全的popFirst)。

集合的相等性比較(==)與你期望的是一致的;如果一個集合中的每個元素都與另一個集合中的元素相等,那麼這兩個集合就是相等的。

如果集合的概念讓你想起了小學時學到的文氏圖,那就太好了,因為集合提供的實例方法可以讓你實現當初學到的所有集合操作。參數可以是集合,也可以是序列(會被轉換為集合);比如,可以是數組、範圍,甚至是字符序列:

intersect、intersectInPlace

找出該集合與參數中都存在的元素。

union、unionInPlace

找出該集合與參數中元素的合集。

exclusiveOr、exclusiveOrInPlace

找出在該集合,但不在參數中的元素,以及在參數,但不在該集合中的元素的合集。

subtract、subtractInPlace

找出在該集合,但不在參數中的元素。

isSubsetOf、isStrictSubsetOf

isSupersetOf、isStrictSupersetOf

返回一個Bool值,判斷該集合中的元素是否都在參數中,或判斷參數中的元素是否都在該集合中。如果兩個集合包含了相同的元素,那麼「strict版本」就會返回false。

isDisjointWith

返回一個Bool值,判斷該集合和參數是否沒有相同的元素。

如下示例演示了如何優雅地使用Set,它來自於我所編寫的一個應用。應用中有很多帶編號的圖片,我們要從中隨機選取一個。不過,我不想選取最近已經選取過的圖片。因此,我維護了一個最近選取過的所有圖片的編號列表。在選取新的圖片時,我會將所有編號的列表轉換為一個Set,同時將最近選取過的圖片的編號列表轉換為一個Set,然後二者相減得到一個未使用過的圖片編號列表!現在,我可以隨機選取一個圖片編號,並將其添加到最近使用過的圖片編號列表中:


let ud = NSUserDefaults.standardUserDefaults
var recents = ud.objectForKey(RECENTS) as? [Int]
if recents == nil {
    recents = 
}
var forbiddenNumbers = Set(recents!)
let legalNumbers = Set(1...PIXCOUNT).subtract(forbiddenNumbers)
let newNumber = Array(legalNumbers)[
    Int(arc4random_uniform(UInt32(legalNumbers.count)))
]
forbiddenNumbers.insert(newNumber)
ud.setObject(Array(forbiddenNumbers), forKey:RECENTS)
  

1.Option Set

Option Set(從技術上來說是OptionSetType)是Swift提供的將Cocoa中常用的一些枚舉類型當作結構體的一種方式。嚴格來說,它並不是Set;不過看起來像是個Set,它通過SetAlgebraType協議實現了Set的諸多特性。因此,Option Set也擁有contains、insert、remove方法,以及各種集合操作方法。

Option Set的目的在於幫助你處理Objective-C的位掩碼。位掩碼是個整型,當同時指定多個選項時,它們用作開關。這種位掩碼在Cocoa中用得非常多。在Objective-C以及Swift 2.0之前,我們通過算術按位或和按位與運算符來操縱位掩碼。這種操作令人感到不可思議,並且極易出錯。多虧了Option Set,在Swift 2.0中,我們可以通過集合操作來操縱位掩碼。

比如,在指定UIView動畫時,我們可以傳遞一個options:實參,它的值來自於UIViewAnimationOptions枚舉,其定義(在Objective-C中)以如下內容開始:


typedef NS_OPTIONS(NSUInteger, UIViewAnimationOptions) {
    UIViewAnimationOptionLayoutSubviews     = 1 << 0,
    UIViewAnimationOptionAllowUserInteraction     = 1 << 1,
    UIViewAnimationOptionBeginFromCurrentState     = 1 << 2,
    UIViewAnimationOptionRepeat     = 1 << 3,
    UIViewAnimationOptionAutoreverse     = 1 << 4,
    // ...
};
  

假設一個NSUInteger是8位(實際上不是,這裡做了一些簡化)。那麼,該枚舉(在Swift中)定義了如下名值對:


UIViewAnimationOptions.LayoutSubviews          0b00000001
UIViewAnimationOptions.AllowUserInteraction    0b00000010
UIViewAnimationOptions.BeginFromCurrentState   0b00000100
UIViewAnimationOptions.Repeat                  0b00001000
UIViewAnimationOptions.Autoreverse             0b00010000
  

可以將這些值組合為單個值(位掩碼),並將其作為動畫的options:實參傳遞過去。為了理解你的意圖,Cocoa只需查看你所傳遞的值中哪些位被設為了1。比如,0b00011000表示UIViewAnimationOptions.Repeat與UIViewAnimationOptions.Autoreverse都為true(也就表示其他都是false)。

問題在於如何構造值0b00011000來傳遞。你可以直接將其構造為字面值,並將options:實參設為UIViewAnimationOptions(rawValue:0b00011000);不過,這麼做可不太好,因為極易出錯,並且會導致代碼難以理解。在Objective-C中,你可以使用算術按位或運算符,這類似於如下Swift代碼:


let val =
    UIViewAnimationOptions.Autoreverse.rawValue |
    UIViewAnimationOptions.Repeat.rawValue
let opts = UIViewAnimationOptions(rawValue: val)
  

不過在Swift 2.0中,UIViewAnimationOptions類型是個Option Set結構體(因為它在Objective-C中被標記為NS_OPTIONS),因此可以像Set一樣使用它。比如,給定一個UIViewAnimationOptions值,你可以通過insert向其添加一個選項:


var opts = UIViewAnimationOptions.Autoreverse
opts.insert(.Repeat)
  

此外,還可以從數組字面值開始,就像初始化Set一樣:


let opts : UIViewAnimationOptions = [.Autoreverse, .Repeat]   

要想不設定選項,請傳遞一個空的Option Set()。這是相對於Swift 1.2及之前的版本一個較大的變化(之前的約定是傳遞nil),這是不合邏輯的,因為該值永遠不會為Optional。

相反的情況是Cocoa傳遞給你一個位掩碼,你想知道是否設置了其中某一位。在這個來自於UITableViewCell子類的示例中,單元格的state以位掩碼的形式傳遞給了我們;我們想要知道表示單元格是否顯示其編輯控件的位信息。過去,我們需要提取出原始值並使用按位與運算符:


override func didTransitionToState(state: UITableViewCellStateMask) {
    let editing = UITableViewCellStateMask.ShowingEditControlMask.rawValue
    if state.rawValue & editing != 0 {
        // ... the ShowingEditControlMask bit is set ...
    }
}
  

這麼做太容易出錯了。在Swift 2.0中,它是個Option Set,因此使用contains方法即可:


override func didTransitionToState(state: UITableViewCellStateMask) {
    if state.contains(.ShowingEditControlMask) {
        // ... the ShowingEditControlMask bit is set ...
    }
}
  

2.Swift Set與Objective-C NSSet

Swift的Set類型會橋接到Objective-C NSSet,中間類型是Set<NSObject>,因為NSObject會被看作Hashable。當然,同樣的規則也適用於數組。Objective-C NSSet要求元素是類實例,Swift則會進行橋接。在實際開發中,你可能會使用一個數組,然後將其轉換為集合或傳遞給需要集合的地方,如下示例來自於我所編寫的代碼:


let types : UIUserNotificationType = [.Alert, .Sound] // a bitmask
let category = UIMutableUserNotificationCategory
category.identifier = "coffee"
let settings = UIUserNotificationSettings( // second parameter is an NSSet
    forTypes: types, categories: [category])
  

如果Objective-C不知道這個Set是什麼類型,那麼從Objective-C返回的就是一個NSObject Set,在這種情況下,你可以對其進行向下類型轉換。不過與NSArray一樣,現在可以對NSSet進行標記以表示其元素類型;很多Cocoa API都已經被標記了,因此無需類型轉換:


override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
    let t = touches.first // an Optional wrapping a UITouch
    // ...
}