《Pro Swift》 第二章:類型(Types)

第一章:語法(Syntax)

我最喜歡的 Swift 單行代碼是使用flatMap()來對一個數(shù)組進行降維和過濾:

let myCustomViews = allViews.flatMap { $0 as? MyCustomView }

這行代碼看起來很簡單,但它包含了很多很棒的 Swift 特性,如果你將其與 Objective-C 中最接近的開箱即用的特性進行比較,就會發(fā)現(xiàn)這些特性最為明顯:

NSArray<MyCustomView *> *myCustomViews = (NSArray<MyCustomView *> *) [allViews filteredArrayUsingPredicate: [NSPredicate predicateWithBlock:^BOOL(id _Nonnull evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
      return [evaluatedObject isKindOfClass:[MyCustomView class]];
}]];

-- Matt Gallagher, CocoaWithLove.com 的作者

高效初始化器(Useful initializers)

理解 Swift 中初始化器是如何工作的并不容易,但這也是你們很久以前學(xué)過的東西,所以我就不在這里重復(fù)了。相反,我想關(guān)注一些有趣的初始化器,它們可能有助你更有效地使用常見的 Swift 類型。

重復(fù)值(Repeating values)

我最喜歡的字符串和數(shù)組初始化器是 repeat:count:,它允許你快速創(chuàng)建大量值。例如,你可以通過在一些文本下面寫等號來創(chuàng)建 Markdown 文本格式的標(biāo)題,如下所示:

This is a heading
=================

Markdown 是一種很有用的格式,因為它可以被計算機解析,同時對人類也具有視覺吸引力,而且下劃線為repeat:count: 提供了一個很好的例子。要使用這個初始化器,為其第一個參數(shù)指定一個字符串,并為其第二個參數(shù)指定重復(fù)的次數(shù),如下所示:

let heading = "This is a heading"
let underline = String(repeating: "=", count: heading.characters.count)

你也可以對數(shù)組這樣做:

let equalsArray = [String](repeating: "=", count: heading.characters.count)

這個數(shù)組初始化器足夠靈活,你可以使用它非常容易地創(chuàng)建多維數(shù)組。例如,這創(chuàng)建了一個準備填充 10x10 數(shù)組:

 var board = [[String]](repeating: [String](repeating: "", count: 10), count: 10)

轉(zhuǎn)換為數(shù)字和從數(shù)字轉(zhuǎn)換(Converting to and from numbers)

當(dāng)我看到這樣的代碼時,我頭疼不已:

let str1 = "\(someInteger)"

這是浪費和不必要的,但是字符串插值是一個很好的特性,使用它是值得原諒。事實上,我很確定我已經(jīng)使用過它好幾次了,毫無疑問!

Swift 有一個簡單、更好的方法,可以使用初始化器根據(jù)整型創(chuàng)建字符串類型:

let str2 = String(someInteger)

當(dāng)使用這種方式進行轉(zhuǎn)換時,事情會變得稍微困難一些,因為你可能會嘗試傳入一個無效的數(shù)字,例如:

let int1 = Int("elephant")

那么,這個初始化器將返回 Int? :如果你給它一個有效的數(shù)字,你會得到一個整數(shù),否則你會得到nil

如果你不想要一個可選值,你應(yīng)該對結(jié)果解包:

if let int2 = Int("1989") {
   print(int2)
}

或者,使用空合操作符(??)提供一個合理的默認值,如下所示:

let int3 = Int("1989") ?? 0
print(int3)

Swift 在這兩個初始化器上有一些處理不同變量基數(shù)的變體。例如,如果你想使用十六進制(以 16 為基數(shù)),你可以讓 Swift 給你一個十六進制數(shù)字的字符串表示形式:

let str3 = String(28, radix: 16)

這將把 str3 設(shè)置為 1c。如果你更喜歡 1C,即大寫——請嘗試以下方法:

let str4 = String(28, radix: 16, uppercase: true)

要將其轉(zhuǎn)換回整數(shù)—請記住它是可選值!——用這個:

let int4 = Int("1C", radix: 16)

唯一的數(shù)組(Unique arrays)

如果你有一個包含重復(fù)值的數(shù)組,并且希望找到一種快速刪除重復(fù)值的方法,則你需要的找的是Set。這是一個內(nèi)建的數(shù)據(jù)類型,具有與普通數(shù)組互相轉(zhuǎn)換的初始化器,這意味著你只需使用初始化器即可快速高效地消除數(shù)組中的重復(fù)數(shù)據(jù):

let scores = [5, 3, 6, 1, 3, 5, 3, 9]
let scoresSet = Set(scores)
let uniqueScores = Array(scoresSet)

這就是它所需要的一切——難怪我這么喜歡集合!

字典的容量(Dictionary capacities)

以一個簡單的初始化器結(jié)尾:如果要單獨向字典添加項,但是知道想添加多少項,請使用minimumCapacity:initializer創(chuàng)建字典,如下所示:

var dictionary = Dictionary<String, String>(minimumCapacity: 100)

這有助于通過預(yù)先分配足夠的空間來快速優(yōu)化執(zhí)行。注意:在后臺,Swift 的字典增加了 2 的冪次方的容量,所以當(dāng)你請求一個像 100 這樣的非 2 的冪次方的容量時,你實際上會得到一個最小容量為 128 的字典。記住,這是最小容量——如果你想添加更多的對象,這不是問題。

枚舉(Enums)

在模式匹配一章中,我已經(jīng)討論了枚舉關(guān)聯(lián)值,但這里我想重點討論枚舉本身,因為它們的功能非常強大。

讓我們從一個非常簡單的枚舉開始,跟蹤一些基本的顏色:

enum Color {
   case unknown
   case blue
   case green
   case pink
   case purple
   case red
}

如果你愿意,可以將所有case項寫在一行上,如下所示:

enum Color {
   case unknown, blue, green, pink, purple, red
}

為了便于測試,讓我們用一個表示玩具的簡單結(jié)構(gòu)體來包裝它:

struct Toy {
   let name: String
   let color: Color
}

Swift 的類型推斷可以推斷出Toycolor屬性是一個Color枚舉,這意味著在創(chuàng)建玩具結(jié)構(gòu)體時不需要編寫Color.blue。例如,我們可以創(chuàng)建兩個這樣的玩具:

let barbie = Toy(name: "Barbie", color: .pink)
let raceCar = Toy(name: "Lightning McQueen", color: .red)

初始值(Raw values)

讓我們從初始值開始:每個枚舉項的基礎(chǔ)數(shù)據(jù)類型。默認情況下,枚舉沒有初始值,因此如果需要初始值,則需要聲明它。例如,我們可以給顏色一個這樣的整型初始值:

enum Color: Int {
   case unknown, blue, green, pink, purple, red
}

只需添加:Int Swift 將為每種顏色都指定了一個匹配的整數(shù),從0 開始向上計數(shù)。也就是說,unknown等于 0blue等于 1 ,以此類推。有時,默認值對你來說并沒有用,所以如果需要,你可以為每個初始值指定單獨的整數(shù)。或者,你可以指定一個不同的起點,使 Xcode 從那里開始計數(shù)。

例如,我們可以像這樣為太陽系的四個行星創(chuàng)建一個枚舉:

enum Planet: Int {
   case mercury = 1
   case venus
   case earth
   case mars
   case unknown
}

通過明確指定水星的值為 1Xcode 將從那里向上計數(shù):金星是 2,地球是 3,火星是 4

現(xiàn)在行星的編號是合理的,我們可以像這樣得到任何的行星的初始值:

let marsNumber = Planet.mars.rawValue

另一種方法并不那么容易:是的,既然我們已經(jīng)有了初始值,你可以從一個數(shù)字創(chuàng)建一個Planet 枚舉,但是這樣做會創(chuàng)建一個可選的枚舉。這是因為你可以嘗試創(chuàng)建一個初始值為 99 的行星,而這個行星并不存在——至少目前還不存在。

幸運的是,我在行星枚舉中添加了一個unknown,當(dāng)請求無效的行星編號時,我們可以從其初始值創(chuàng)建行星枚舉,并使用空值合并運算符提供合理的默認值:

let mars = Planet(rawValue: 556) ?? Planet.unknown

對于行星來說,數(shù)字是可以的,但是當(dāng)涉及到顏色時,你可能會發(fā)現(xiàn)使用字符串更容易。除非你有非常特殊的需要,否則只需指定String作為枚舉的原始數(shù)據(jù)類型就足以為它們提供有意義的名稱—— Swift 會自動將你的枚舉名稱映射到一個字符串。例如,這將打印 Pink:

enum Color: String {
   case unknown, blue, green, pink, purple, red
}
let pink = Color.pink.rawValue
print(pink)

不管初始值的數(shù)據(jù)類型是什么,或者是否有初始值,當(dāng)枚舉被用作字符串插值的一部分時,Swift 都會自動對枚舉進行字符串化。但是,以這種方式使用并不會使它們變成字符串,所以如果你想調(diào)用任何字符串方法,你需要自己根據(jù)它們創(chuàng)建一個字符串。例如:

let barbie = Toy(name: "Barbie", color: .pink)
let raceCar = Toy(name: "Lightning McQueen", color: .red)
// regular string interpolation
print("The \(barbie.name) toy is \(barbie.color)")
// get the string form of the Color then call a method on it
print("The \(barbie.name) toy is \(barbie.color.rawValue.uppercased())")

計算屬性和方法(Computed properties and methods)

枚舉沒有結(jié)構(gòu)體和類那么強大,但是它們允許你在其中封裝一些有用的功能。例如,除非枚舉存儲的屬性是靜態(tài)的,否則不能給它們賦值,因為這樣做沒有意義,但是你可以添加在運行一些代碼之后返回值的計算屬性。

為了讓你了解一些有用的內(nèi)容,讓我們向Color枚舉添加一個計算屬性,該屬性將打印顏色的簡要描述。

enum Color {
   case unknown, blue, green, pink, purple, red
   var description: String {
      switch self {
      case .unknown:
         return "the color of magic"
      case .blue:
         return "the color of the sky"
      case .green:
         return "the color of grass"
      case .pink:
         return "the color of carnations"
      case .purple:
         return "the color of rain"
      case .red:
         return "the color of desire"
      }
   } 
}
let barbie = Toy(name: "Barbie", color: .pink)
print("This \(barbie.name) toy is \(barbie.color.description)")

當(dāng)然,計算屬性只是封裝方法的語法糖,所以你可以直接將方法添加到枚舉中也就不足為奇了。現(xiàn)在讓我們通過向Color 枚舉添加兩個新方法來實現(xiàn)這一點,forBoys()forGirls(),根據(jù)顏色來判斷一個玩具是為女孩還是男孩準備的——只需在我們剛剛添加的description 屬性下面添加以下內(nèi)容:

func forBoys() -> Bool {
   return true
}
func forGirls() -> Bool {
   return true
}

如果你想知道,根據(jù)顏色來決定哪個玩具是男孩的還是女孩的有點上世紀 70 年代的味道:這些方法都返回true是有原因的!

因此:我們的枚舉現(xiàn)在有一個初始值、一個計算屬性和一些方法。我希望你能明白為什么我把枚舉描述為看起來很強大——它們可以做很多事情!

數(shù)組(Arrays)

數(shù)組是 Swift 的真正主力之一。當(dāng)然,它們在大多數(shù)應(yīng)用程序中都很重要,但是它們對泛型的使用使它們在添加一些有用功能的同時保證類型安全。我不打算詳細介紹它們的基本用法;相反,我想向你介紹一些你可能不知道的有用方法。

第一:排序。只要數(shù)組存儲的元素類型遵循Comparable協(xié)議,就會得到sorted()sort()方法——前者返回一個已排序的數(shù)組,而后者修改調(diào)用它的數(shù)組。如果你不打算遵循Comparable協(xié)議,可以使用sorted()sort()的替代版本,讓你指定數(shù)據(jù)項應(yīng)該如何排序。

為了演示下面的例子,我們將使用這兩個數(shù)組:

var names = ["Taylor", "Timothy", "Tyler", "Thomas", "Tobias", "Tabitha"]
let numbers = [5, 3, 1, 9, 5, 2, 7, 8]

要按字母順序排列names數(shù)組,使用sorted()sort()方法取決于你的需要。

let sorted = names.sorted()

一旦代碼運行,sorted將包含["Tabitha", "Taylor", "Thomas", "Timothy", "Tobias", "Tyler"]

如果你想編寫自己的排序函數(shù) - 如果你不采用Comparable則是必需的,否則是可選的 - 編寫一個接受兩個字符串的閉包,如果第一個字符串應(yīng)該在排在第二個字符串之前,則返回true

例如,我們可以編寫一個字符串排序算法,它的行為與常規(guī)的字母排序相同,但它總是將名稱 Taylor 放在前面。我敢肯定,這正是Taylor Swift(美國女歌手)想要的:

names.sort {
   print("Comparing \($0) and \($1)")
   if ($0 == "Taylor") {
      return true
   } else if $1 == "Taylor" {
      return false
   } else {
      return $0 < $1
  } 
}

該代碼使用sort()而不是sorted(),這將使數(shù)組按適當(dāng)位置排序,而不是返回一個新的排序數(shù)組。我還在其中添加了一個print()調(diào)用,這樣你就可以確切地看到sort()是如何工作的。這是輸出結(jié)果:

Comparing Timothy and Taylor
Comparing Tyler and Timothy
Comparing Thomas and Tyler
Comparing Thomas and Timothy
Comparing Thomas and Taylor
Comparing Tobias and Tyler
Comparing Tobias and Timothy
Comparing Tabitha and Tyler
Comparing Tabitha and Tobias
Comparing Tabitha and Timothy
Comparing Tabitha and Thomas
Comparing Tabitha and Taylor

如你所見,隨著算法的發(fā)展,名稱可以顯示為 $0$1,這就是為什么我在自定義排序函數(shù)中比較這兩種可能性的原因。

排序很容易,但采用Comparable還可以實現(xiàn)兩個更有用的方法:min()max()。就像sort()一樣,如果不采用Comparable的方法,這些方法也可以接受一個閉包,但是代碼是相同的,因為操作是相同的:A項應(yīng)該出現(xiàn)在B項之前嗎?

使用前面的number數(shù)組,我們可以在兩行代碼中找到數(shù)組中的最高值和最低值:

let lowest = numbers.min()
let highest = numbers.max()

對于字符串,min()返回排序后的第一個字符串,max()返回最后一個字符串。如果你嘗試重用我為自定義排序提供的相同閉包,包括print()語句,你將看到min()max()實際上比使用sort()更高效,因為它們不需要移動每一項。

遵循Comparable協(xié)議(Conforming to Comparable)

對于字符串和整型等基本數(shù)據(jù)類型,使用sort()min()max()非常簡單。但是你怎么把別的東西完全分類呢,比如奶酪的種類或者狗的品種?我已經(jīng)向你展示了如何編寫自定義閉包,但是如果你必須進行多次排序,那么這種方法就會變得非常麻煩—你最終會復(fù)制代碼,這將帶來維護的噩夢。

更聰明的解決方案是實現(xiàn)Comparable協(xié)議,這反過來要求你使用操作符重載。稍后我們將對此進行更詳細的討論,但現(xiàn)在我只想向你展示足以進行比較的工作。首先,這里有一個基本的Dog結(jié)構(gòu),它包含一些信息:

struct Dog {
   var breed: String
   var age: Int
}

為了便于測試,我們將創(chuàng)建三只 dog 并將它們放到數(shù)組里:

let poppy = Dog(breed: "Poodle", age: 5)
let rusty = Dog(breed: "Labrador", age: 2)
let rover = Dog(breed: "Corgi", age: 11)
var dogs = [poppy, rusty, rover]

因為 Dog結(jié)構(gòu)體沒有遵循 Comparable協(xié)議,所以我們沒有在dogs數(shù)組上獲得簡單的sort()ordered()方法,我們只獲得了需要自定義閉包才能運行的方法。

要使Dog遵循 Comparable協(xié)議,如下所示:

struct Dog: Comparable {
   var breed: String
   var age: Int
}

你會得到錯誤,沒關(guān)系。

下一步是讓第一次嘗試它的人感到困惑的地方:你需要實現(xiàn)兩個新函數(shù),但是它們有一些不同尋常的名稱,在處理操作符重載時需要一點時間來適應(yīng),這正是我們需要做的。

Dog結(jié)構(gòu)中添加這兩個函數(shù):

static func <(lhs: Dog, rhs: Dog) -> Bool {
   return lhs.age < rhs.age
}
static func ==(lhs: Dog, rhs: Dog) -> Bool {
   return lhs.age == rhs.age
}

需要說明的是,你的代碼應(yīng)該如下所示:

struct Dog: Comparable {
   var breed: String
   var age: Int
   static func <(lhs: Dog, rhs: Dog) -> Bool {
      return lhs.age < rhs.age
   }
   static func ==(lhs: Dog, rhs: Dog) -> Bool {
      return lhs.age == rhs.age
   }
}

如果你以前沒有使用過運算符重載,那么這些函數(shù)名是不常見的,但是我希望你能夠確切地了解它們的作用: 當(dāng)你編寫dog1 < dog2時使用 < 函數(shù),當(dāng)你寫dog1 == dog2時使用==函數(shù)。

這兩個步驟足以完全實現(xiàn)Comparable協(xié)議,因此你現(xiàn)在可以輕松地對dogs數(shù)組進行排序:

dogs.sort()

添加和刪除元素(Adding and removing items)

幾乎可以肯定,你已經(jīng)使用過數(shù)組的append()insert()remove(at:)方法,但我想確保你知道添加和刪除項的其他方法。

如果想將兩個數(shù)組相加,可以使用++=來就地相加。例如:

let poppy = Dog(breed: "Poodle", age: 5)
let rusty = Dog(breed: "Labrador", age: 2)
let rover = Dog(breed: "Corgi", age: 11)
var dogs = [poppy, rusty, rover]
let beethoven = Dog(breed: "St Bernard", age: 8)
dogs += [beethoven]

當(dāng)涉及到刪除項目時,有兩種有趣的方法可以刪除最后一項:removeLast()popLast()。它們都刪除數(shù)組中的最后一項并將其返回給你,但是popLast()返回的是可選值,而removeLast()不是。考慮一下:dogs.removeLast()必須返回Dog結(jié)構(gòu)的一個實例。 如果數(shù)組是空的會發(fā)生什么?答案是“壞事情”——你的應(yīng)用會崩潰。

如果你試圖刪除一項時,你的數(shù)組可能是空的,那么使用popLast(),這樣你就可以安全地檢查返回值:

if let dog = dogs.popLast() {
   // do stuff with `dog`
}

注意:removeLast()有一個稱為removeFirst()的對應(yīng)項,用于刪除和返回數(shù)組中的初始項。遺憾的是,popLast()沒有類似的方法。

空和容量(Emptiness and capacity)

下面是我想展示的另外兩個小技巧: isEmptyreserveCapacity()

第一個是isEmpty,如果數(shù)組沒有添加任何項,則返回true。 這比使用someArray.count == 0更短,更有效,但由于某種原因使用較少。

reserveCapacity()方法允許您告訴 iOS 打算在數(shù)組中存儲多少項。這并不是一個嚴格的限制。如果你預(yù)留了 10 個容量,你可以繼續(xù)存儲 20 個,如果你想的話——但它允許 iOS 優(yōu)化對象存儲,確保你有足夠的空間來容納你的建議容量。

警告:使用 reserveCapacity()不是一個免費的操作。在后臺,Swift 將創(chuàng)建一個包含相同值的新數(shù)組,并為你需要的容量留出空間。它不只是擴展現(xiàn)有數(shù)組。這樣做的原因是該方法保證得到的數(shù)組將具有連續(xù)存儲(即所有項目彼此相鄰存儲而不是分散在 RAM 中)因此,Swift 會做大量的移動操作。即使你已經(jīng)調(diào)用了reserveCapacity(),這也適用—嘗試將這段代碼放到一個 Playground 中,自己看看:

import Foundation
let start = CFAbsoluteTimeGetCurrent()
var array = Array(1...1000000)
array.reserveCapacity(1000000)
array.reserveCapacity(1000000)
let end = CFAbsoluteTimeGetCurrent() - start
print("Took \(end) seconds")

當(dāng)這段代碼運行時,你將看到調(diào)用reserveCapacity()時兩次都會出現(xiàn)嚴重的暫停。因為reserveCapacity()是一個O(n)復(fù)雜度的調(diào)用(其中n是數(shù)組的count值),所以應(yīng)該在向數(shù)組添加項之前調(diào)用它。

連續(xù)數(shù)組(Contiguous arrays)

Swift 提供了兩種主要的數(shù)組,但幾乎總是只使用一種。首先,讓我們分解一下語法:你應(yīng)該知道這兩行代碼在功能上是相同的:

let array1 = [Int]()
let array2 = Array<Int>()

第一行是第二行的語法糖。到目前為止,一切都很簡單。但是我想向你們介紹一下連續(xù)數(shù)組容器的重要性,它看起來是這樣的:

let array3 = ContiguousArray<Int>(1...1000000)

就是這樣。連續(xù)數(shù)組具有你習(xí)慣使用的所有屬性和方法—countsort()min()map()等等—但因為所有項都保證是連續(xù)存儲的,即你可以得到更好的表現(xiàn)。

蘋果的官方文檔說,當(dāng)你需要 C 數(shù)組的性能時,應(yīng)該使用連續(xù)數(shù)組,而當(dāng)你想要針對 Cocoa 高效轉(zhuǎn)換優(yōu)化時,應(yīng)該使用常規(guī)數(shù)組。文檔還說,當(dāng)與非類類型一起使用時,ArrayContiguousArray的性能是相同的,這意味著在使用類時,你肯定會得到性能上的改進。

原因很簡單:Swift 數(shù)組可以橋接到NSArray,這是 Objective-C 開發(fā)人員使用的數(shù)組類型。 由于歷史原因,NSArray無法存儲值類型,例如整數(shù),除非它們被包裝在對象中。 因此,Swift 編譯器可以很聰明:如果你創(chuàng)建一個包含值類型的常規(guī) Swift 數(shù)組,它就知道你不能嘗試將它橋接到NSArray,因此它可以執(zhí)行額外的優(yōu)化來提高性能。

也就是說,我發(fā)現(xiàn)連續(xù)數(shù)組無論如何都比數(shù)組快,即使是使用Int這樣的基本類型。舉個簡單的例子,下面的代碼把1到100萬的數(shù)字加起來:

let array2 = Array<Int>(1...1000000)
let array3 = ContiguousArray<Int>(1...1000000)
var start = CFAbsoluteTimeGetCurrent()
array2.reduce(0, +)
var end = CFAbsoluteTimeGetCurrent() - start
print("Took \(end) seconds")
start = CFAbsoluteTimeGetCurrent()
array3.reduce(0, +)
end = CFAbsoluteTimeGetCurrent() - start
print("Took \(end) seconds")

當(dāng)我運行這段代碼時,數(shù)組花費 0.25 秒,連續(xù)數(shù)組花費 0.13 秒。考慮到我們只是循環(huán)了超過 100 萬個元素,這并不是非常優(yōu)秀,但如果你想在你的應(yīng)用程序或游戲中獲得額外的性能提升,你肯定應(yīng)該嘗試使用連續(xù)數(shù)組。

集合(Sets)

了解集合和數(shù)組之間的區(qū)別 – 并知道哪一個在何時是正確的選擇 - 是任何 Swift 開發(fā)人員工具箱中的一項重要技能。集合可以被認為是無序數(shù)組,不能包含重復(fù)元素。如果你多次添加同一個元素,它將只在集合中出現(xiàn)一次。缺少重復(fù)項和不跟蹤順序的組合允許集合比數(shù)組快得多,因為數(shù)據(jù)項是根據(jù)哈希而不是遞增的整數(shù)索引存儲的。

要將其置于上下文中,檢查數(shù)組是否包含項,復(fù)雜度為O(n),這意味著“它取決于你在數(shù)組中有多少元素”。這是因為Array.contains()需要從 0 開始檢查每個元素,所以如果有 50 個元素,則需要執(zhí)行 50 次檢查。檢查一個集合是否包含項,復(fù)雜度為O(1),這意味著“無論你有多少元素,它始終以相同的速度運行”。這是因為集合的工作原理類似于字典:通過創(chuàng)建對象的 hash 生成鍵,而該鍵直接指向?qū)ο蟠鎯Φ奈恢谩?/p>

基礎(chǔ)(The basics)

最好的實驗方法是使用 Playground ,試著輸入這個:

var set1 = Set<Int>([1, 2, 3, 4, 5])

當(dāng)它運行時,你將在輸出窗口中看到 { 5,2,3,1,4 }。就像我說的,集合是無序的,所以你可能會在 Xcode 窗口中看到一些不同的東西。

這將從數(shù)組中創(chuàng)建一個新的集合,但是你也可以從范圍中創(chuàng)建它們,就像數(shù)組一樣:

var set2 = Set(1...100)

你還可以單獨向它們添加項,盡管方法名為insert()而不是append(),以反映其無序性:

set1.insert(6)
set1.insert(7)

若要檢查集合中是否存在項,請使用像閃電一樣快的contains()方法:

if set1.contains(3) {
   print("Number 3 is in there!")
}

并使用remove()從集合中刪除項:

set1.remove(3)

數(shù)組和集合(Arrays and sets)

數(shù)組和集合一起使用時工作得很好,所以它們幾乎可以互換也就不足為奇了。首先,數(shù)組和集合都有接受另一種類型的構(gòu)造函數(shù),如下所示:

var set1 = Set<Int>([1, 2, 3, 4, 5])
var array1 = Array(set1)
var set2 = Set(array1)

實際上,將數(shù)組轉(zhuǎn)換為集合并返回是刪除所有重復(fù)項的最快方法,而且只需兩行代碼。

其次,集合的一些方法返回數(shù)組而不是集合,因為這樣做更有用。例如,集合上的ordered()map()filter()方法返回一個數(shù)組。

所以,雖然你可以像這樣直接循環(huán)集合:

for number in set1 {
   print(number)
}

…你也可以先將集合按合理的順序排序,如下所示:

for number in set1.sorted() {
   print(number)
}

像數(shù)組一樣,集合使用removeFirst()方法從集合的前面刪除項。 但是它的用途是不同的:因為集合是無序的,你真的不知道第一個項目是什么,所以removeFirst()實際上意味著“給我任何對象,以便我可以處理它。” 巧妙地,集合有一個popFirst()方法,而數(shù)組沒有——我真希望知道為什么!

集合操作(Set operations)

集合附帶了許多方法,允許你以有趣的方式操作它們。例如,你可以創(chuàng)建兩個集合的并集,即兩個集合的合并,如下所示:

let spaceships1 = Set(["Serenity", "Nostromo", "Enterprise"])
let spaceships2 = Set(["Voyager", "Serenity", "Executor"])
let union = spaceships1.union(spaceships2)

當(dāng)代碼運行時,union將包含 5 個條目,因為重復(fù)的 “Serenity” 只出現(xiàn)一次。

另外兩個有用的集合操作是intersection()symmetricDifference()。前者返回一個只包含兩個集合中存在的元素的新集合,而后者則相反:它只返回兩個集合中不存在的元素。代碼是這樣的:

let intersection = spaceships1.intersection(spaceships2)
let difference = spaceships1.symmetricDifference(spaceships2)

當(dāng)它運行時,intersection將包含Serenitydifference將包含NostromoEnterpriseVoyagerExecutor

注意:union()intersection()symmetricDifference()都有直接修改集合的替代方法,可以通過向方法前添加form來調(diào)用它們,formUnion()formIntersection()formSymmetricDifference()

集合有幾個查詢方法,根據(jù)提供的內(nèi)容返回truefalse

這些方法是:

  • A.isSubset(of: B): 如果集合 A 的所有項都在集合 B 中,則返回 true
  • A.isSuperset(of: B): 如果集合 B 的所有項都在集合 A 中,則返回 true
  • A.isDisjoint(with: B): 如果集合 B 的所有項都不在集合 A 中,則返回 true
  • A.isStrictSubset(of: B): 如果集合 A 的所有項都在集合 B 中,則返回 true , 但是 AB 不相等
  • A.isStrictSuperset(of: B): 如果集合 B 的所有項都在集合 A 中,則返回 true ,但是 AB 不相等

集合區(qū)分子集和嚴格子集,不同之處在于后者必須排除相同的集合。 也就是說,如果集合 A 中的每個項目也在集合 B 中,則集合 A 是集合 B 的子集。另一方面,如果集合 A 中的每個元素也在集合 B 中,則集合 A 是集合 B 的嚴格子集,但是集合 B至少包含集合 A 中缺少的一個項。

下面的代碼分別演示了它們,我在注釋中標(biāo)記了每個方法的返回值:

let spaceships1 = Set(["Serenity", "Nostromo", "Enterprise"])
let spaceships2 = Set(["Voyager", "Serenity", "StarDestroyer"])
let spaceships3 = Set(["Galactica", "Sulaco", "Minbari"])
let spaceships1and2 = spaceships1.union(spaceships2)
spaceships1.isSubset(of: spaceships1and2) // true
spaceships1.isSubset(of: spaceships1) // true
spaceships1.isSubset(of: spaceships2) // false
spaceships1.isStrictSubset(of: spaceships1and2) // true
spaceships1.isStrictSubset(of: spaceships1) // false
spaceships1and2.isSuperset(of: spaceships2) // true
spaceships1and2.isSuperset(of: spaceships3) // false
spaceships1and2.isStrictSuperset(of: spaceships1) // true
spaceships1.isDisjoint(with: spaceships2) // false

NSCountedSet

Foundation 庫有一個專門的集合叫做NSCountedSet,它是一個具有扭曲(twist)的集合: 項仍然只能出現(xiàn)一次,但是如果你嘗試多次添加它們,它將跟蹤計數(shù),就像它們確實存在一樣。這意味著你可以獲得非重復(fù)集合的所有速度,但是如果允許重復(fù),你還可以計算項目出現(xiàn)的次數(shù)。

你可以根據(jù)需要從 Swift 數(shù)組或集合創(chuàng)建NSCountedSet。在下面的例子中,我創(chuàng)建了一個大型數(shù)組(帶有重復(fù)項),將它全部添加到計數(shù)集,然后打印出兩個值的計數(shù):

var spaceships = ["Serenity", "Nostromo", "Enterprise"]
spaceships += ["Voyager", "Serenity", "Star Destroyer"]
spaceships += ["Galactica", "Sulaco", "Minbari"]
let countedSet = NSCountedSet(array: spaceships)
print(countedSet.count(for: "Serenity")) // 2
print(countedSet.count(for: "Sulaco")) // 1

正如你所看到的,您可以使用count(for:)來檢索一個元素在計數(shù)集合中出現(xiàn)的次數(shù)(理論上)。你可以使用countedSet.allObjects屬性提取所有對象的數(shù)組,但要注意:NSCountedSet不支持泛型,因此你需要將其類型轉(zhuǎn)換回[String]

元組(Tuples)

元組類似于簡化的匿名結(jié)構(gòu)體:它們是攜帶不同信息字段的值類型,但不需要正式定義。由于缺少正式的定義,所以很容易創(chuàng)建和丟棄它們,所以當(dāng)你需要一個函數(shù)返回多個值時,通常會使用它們。

在關(guān)于模式匹配和析構(gòu)的章節(jié)中,我介紹了元組如何以其他方式使用——它們確實是無處不在的小東西。有多普遍?那么,考慮以下代碼:

func doNothing() { }
let result = doNothing()

思考一下: result常量具有什么數(shù)據(jù)類型?你可能已經(jīng)猜到了本章的名稱,它是一個元組: ()。在后臺,SwiftVoid 數(shù)據(jù)類型(沒有顯式返回類型的函數(shù)的默認值)映射到一個空元組。

現(xiàn)在考慮一下這個:Swift 中的每一種類型——整數(shù)、字符串等等——實際上都是自身的一個單元素元組。請看下面的代碼:

let int1: (Int) = 1
let int2: Int = (1)

這段代碼完全正確:將一個整數(shù)賦值給一個單元素元組和將一個單元素元組賦值給一個整數(shù)都做了完全相同的事情。正如 Apple 文檔中所說,“如果括號中只有一個元素,那么(元組的)類型就是該元素的類型。”它們實際上是一樣的,所以你甚至可以這樣寫:

var singleTuple = (value: 42)
singleTuple = 69

當(dāng) Swift 編譯第一行時,它基本上忽略標(biāo)簽,將其變成一個包含整數(shù)的單元素元組——而整數(shù)又與整數(shù)相同。實際上,這意味著你不能給單元素元組添加標(biāo)簽——如果你試圖強制一個數(shù)據(jù)類型,你會得到一個錯誤:

var thisIsAllowed = (value: 42)
var thisIsNot: (value: Int) = (value: 42)

如果你沒有從一個函數(shù)返回任何東西,你得到一個元組,如果你從一個函數(shù)返回幾個值,你得到一個元組,如果你返回一個值,你實際上也得到一個元組。我認為可以肯定地說,不管您是否知道,您已經(jīng)是一個頻繁使用元組的用戶了!

現(xiàn)在,我將在下面介紹元組的一些有趣的方面,但是首先你應(yīng)該知道元組有幾個缺點。具體來說,你不能向元組添加方法或讓它們實現(xiàn)協(xié)議——如果這是你想要做的,那么你要尋找的是結(jié)構(gòu)體。

元組有類型(Tuples have types)

元組很容易被認為是數(shù)據(jù)的開放垃圾場,但事實并非如此:它們是強類型的,就像 Swift 中的其他所有東西一樣。這意味著你不能改變一個元組的類型一旦它被創(chuàng)建-像這樣的代碼將無法編譯:

var singer = ("Taylor", "Swift")
singer = ("Taylor", "Swift", 26)

如果你不給元組的元素命名,你可以使用從 0 開始的數(shù)字來訪問它們,就像這樣:

var singer = ("Taylor", "Swift")
print(singer.0)

如果元組中有元組(這并不少見),則需要使用 0.0 ,諸如此類:

var singer = (first: "Taylor", last: "Swift", address: ("555 Taylor Swift Avenue", "No, this isn't real", "Nashville"))
print(singer.2.2) // Nashville

這是一種內(nèi)置的行為,但并不意味著推薦使用它。你可以——通常也應(yīng)該——給你的元素命名,這樣你才能更明智地訪問它們:

var singer = (first: "Taylor", last: "Swift")
print(singer.last)

這些名稱是類型的一部分,所以這樣的代碼不會編譯通過:

var singer = (first: "Taylor", last: "Swift")
singer = (first: "Justin", fish: "Trout")

元組和閉包(Tuples and closures)

不能向元組添加方法,但可以添加閉包。我同意這種區(qū)別很好,但它很重要:向元組添加閉包就像添加任何其他值一樣,實際上是將代碼作為數(shù)據(jù)類型附加到元組。因為它不是一個方法,聲明有一點不同,但這里有一個例子讓你開始:

var singer = (first: "Taylor", last: "Swift", sing: { (lyrics: String) in
   print("\(lyrics)")
})

singer.sing("Haters gonna hate")

注意:這些閉包不能訪問同級元素,這意味著這樣的代碼不能工作:

print("My name is \(first): \(lyrics)")

返回多個值(Returning multiple values)

元組通常用于從一個函數(shù)返回多個值。事實上,如果這是元組帶給我們的唯一東西,那么與其他語言(包括 Objective-C ) 相比,它們?nèi)匀皇?Swift 的一個重要特性。

下面是一個 Swift 函數(shù)在一個元組中返回多個值的例子:

func fetchWeather() -> (type: String, cloudCover: Int, high: Int, low: Int) {
   return ("Sunny", 50, 32, 26)
}
let weather = fetchWeather()
print(weather.type)

當(dāng)然,你不必指定元素的名稱,但是這無疑是一種很好的實踐,這樣其他開發(fā)人員就知道應(yīng)該期望什么。

如果你更喜歡析構(gòu)元組返回函數(shù)的結(jié)果,那么也很容易做到:

let (type, cloud, high, low) = fetchWeather()

相比之下,如果 Swift 沒有元組,那么我們將不得不依賴于返回一個數(shù)組和按需要進行類型轉(zhuǎn)換,如下所示:

import Foundation
func fetchWeather() -> [Any] {
   return ["Sunny", 50, 32, 26]
}
let weather = fetchWeather()
let weatherType = weather[0] as! String
let weatherCloud = weather[1] as! Int
let weatherHigh = weather[2] as! Int
let weatherLow = weather[3] as! Int

或者更糟的是,使用inout變量,如下所示:

func fetchWeather(type: inout String, cloudCover: inout Int, high: inout Int, low: inout Int) {
    type = "Sunny"
    cloudCover = 50
    high = 32
    low = 26
 }
var weatherType = ""
var weatherCloud = 0
var weatherHigh = 0
var weatherLow = 0
fetchWeather(type: &weatherType, cloudCover: &weatherCloud, high: &weatherHigh, low: &weatherLow)

說真的:如果inout是答案,你可能問錯了問題。

可選元組(Optional tuples)

元組可以包含可選元素,也可以有可選元組。這聽起來可能相似,但差別很大:可選元素是元組中的單個項,如String?Int? ,而可選元組是整個結(jié)構(gòu)可能存在也可能不存在。

具有可選元素的元組必須存在,但其可選元素可以為nil。可選元組必須填充其所有元素,或者是nil。具有可選元素的可選元組可能存在,也可能不存在,并且其每個可選元素可能存在,也可能不存在。

當(dāng)處理可選元組時,Swift 不能使用類型推斷,因為元組中的每個元素都有自己的類型。所以,你需要明確聲明你想要什么,就像這樣:

let optionalElements: (String?, String?) = ("Taylor", nil)
let optionalTuple: (String, String)? = ("Taylor", "Swift")
let optionalBoth: (String?, String?)? = (nil, "Swift")

一般來說,可選元素很常見,可選元組就不那么常見了。

比較元組(Comparing tuples)

Swift 允許你比較最多擁有 6 個參數(shù)數(shù)量的元組,只要它們具有相同的類型。這意味著您可以使用==比較包含最多 6 個項的元組,如果一個元組中的所有 6 個項都匹配第二個元組中的對應(yīng)項,則返回true

例如,下面的代碼會打印“No match”:

let singer = (first: "Taylor", last: "Swift")
let person = (first: "Justin", last: "Bieber")
if singer == person {
   print("Match!")
} else {
   print("No match")
}

但是要注意:元組比較忽略了元素標(biāo)簽,只關(guān)注類型,這可能會產(chǎn)生意想不到的結(jié)果。例如,下面的代碼將打印“Match!”,即使元組標(biāo)簽不同:

let singer = (first: "Taylor", last: "Swift")
let bird = (name: "Taylor", breed: "Swift")
if singer == bird {
   print("Match!")
} else {
   print("No match")
}

別名(Typealias)

你已經(jīng)看到了元組是多么強大、靈活和有用,但是有時候你可能想要將一些東西形式化。給你一個斯威夫特主題的例子,考慮這兩個元組,代表泰勒·斯威夫特的父母:

let father = (first: "Scott", last: "Swift")
let mother = (first: "Andrea", last: "Finlay")

(不,我沒有泰勒·斯威夫特的資料,但我可以用維基百科!)

當(dāng)他們結(jié)婚時,安德里亞·芬利變成了安德里亞·斯威夫特,他們成為了夫妻。我們可以寫一個簡單的函數(shù)來表示這個事件:

func marryTaylorsParents(man: (first: String, last: String), woman: (first: String, last: String)) -> (husband: (first: String, last: String), wife: (first: String, last: String)) {
   return (man, (woman.first, man.last))
}

注:我用了 “man” 和 “wife” ,還讓妻子改成了她丈夫的姓,因為 Taylor Swift 的父母就是這么做的。很明顯,這只是一種婚姻形式,我希望你能理解這是一個簡化的例子,而不是一個政治聲明。

father元組和mother元組單獨看起來足夠好,但是marryTaylorsParents()函數(shù)看起來相當(dāng)糟糕。一次又一次地重復(fù)(first: String, last: String)會使它很難閱讀,也很難更改。

Swift 的解決方案很簡單: typealias關(guān)鍵字。這并不是特定于元組的,但在這里它無疑是最有用的:它允許你為類型附加一個替代名稱。例如,我們可以創(chuàng)建這樣一個 typealias

typealias Name = (first: String, last: String)

使用這個函數(shù),marryTaylorsParents()函數(shù)明顯變短:

func marryTaylorsParents(man: Name, woman: Name) -> (husband: Name, wife: Name) {
   return (man, (woman.first, man.last))
}

范型(Generics)

盡管泛型在 Swift 中是一個高級主題,但你一直在使用它們:[String]是你使用數(shù)組結(jié)構(gòu)存儲字符串的一個例子,這是泛型的一個例子。事實上,使用泛型很簡單,但是創(chuàng)建泛型需要一點時間來適應(yīng)。在本章中,我將演示如何(以及為什么!)創(chuàng)建自己的泛型,從函數(shù)開始,然后是結(jié)構(gòu)體,最后是包裝 Foundation類型。

讓我們從一個簡單的問題開始,這個問題演示了泛型是什么以及它們?yōu)槭裁粗匾何覀儗?chuàng)建一個非常簡單的泛型函數(shù)。

設(shè)想一個函數(shù),它被設(shè)計用來打印關(guān)于字符串的一些調(diào)試信息。它可能是這樣的:

func inspectString(_ value: String) {
   print("Received String with the value \(value)")
}
inspectString("Haters gonna hate")

現(xiàn)在讓我們創(chuàng)建相同的函數(shù)來打印關(guān)于整數(shù)的信息:

func inspectInt(_ value: Int) {
   print("Received Int with the value \(value)")
}
inspectInt(42)

現(xiàn)在讓我們創(chuàng)建打印關(guān)于 Double 類型的信息的相同函數(shù)。實際上……我們不需要。這顯然是非常枯燥的代碼,我們需要將其擴展到浮點數(shù)、布爾值、數(shù)組、字典等等。有一種更智能的解決方案稱為泛型編程,它允許我們編寫處理稍后指定類型的函數(shù)。Swift 中的通用代碼使用尖括號<>,所以它非常明顯!

要創(chuàng)建inspectString()函數(shù)的泛型形式,可以這樣寫:

func inspect<SomeType>(_ value: SomeType) { }

注意SomeType的用法:在函數(shù)名后面的尖括號中,用于描述value參數(shù)。尖括號里的第一個是最重要的,因為它定義了你的占位符數(shù)據(jù)類型:inspect<SomeType>()意味著“一個名為inspect()的函數(shù),可以使用任何類型的數(shù)據(jù)類型,但是無論使用的數(shù)據(jù)類型是什么,我想把它稱為SomeType。因此,參數(shù)value: SomeType現(xiàn)在應(yīng)該更有意義了:SomeType將被用于調(diào)用函數(shù)的任何數(shù)據(jù)類型替換。

稍后你將看到,占位符數(shù)據(jù)類型也用于返回值。但是首先,這里是inspect()函數(shù)的最終版本,它輸出正確的信息,無論向它拋出什么數(shù)據(jù):

func inspect<T>(_ value: T) {
   print("Received \(type(of: value)) with the value \(value)")
}

inspect("Haters gonna hate")
inspect(56)

我使用了type(of:)函數(shù),以便 Swift正確地輸出 “String”、“Int” 等。注意,我還使用了T而不是某種類型,這是一種常見的編碼約定:第一個占位符數(shù)據(jù)類型名為T,第二個U和第三個V,以此類推。在實踐中,我發(fā)現(xiàn)這個約定沒有幫助,也不清楚,所以盡管我將在這里使用它,只是因為你必須習(xí)慣它。

現(xiàn)在,你可能想知道泛型給這個函數(shù)帶來了什么好處——難道它就不能為它的參數(shù)類型使用泛型嗎?在這種情況下可以,因為占位符只使用一次,所以這在功能上是相同的:

func inspect(_ value: Any) {
   print("Received \(type(of: value)) with the value \(value)")
}

但是,如果我們希望函數(shù)接受相同類型的兩個參數(shù),那么Any和占位符之間的區(qū)別就會變得更加明顯。例如:

func inspect<T>(_ value1: T, _ value2: T) {
   print("1. Received \(type(of: value1)) with the value \(value1)")
   print("2. Received \(type(of: value2)) with the value \(value2)") 
}

現(xiàn)在接受T類型的兩個參數(shù),這是占位符數(shù)據(jù)類型。同樣,我們不知道這將是什么,這就是為什么我們給它一個抽象的名稱,如 “T ”,而不是一個特定的數(shù)據(jù)類型,如IntString。然而,這兩個參數(shù)的類型都是T,這意味著無論最終是什么類型,它們都必須是相同的類型。所以,這個代碼是合法的:

inspect(42, 42)

但這是行不通的,因為它混合了數(shù)據(jù)類型:

inspect(42, "Dolphin")

如果我們對數(shù)據(jù)類型使用了Any參數(shù),那么 Swift 就不能確保兩個參數(shù)都是相同的類型——一個可以是Int,另一個可以是String。所以,這段代碼將是正確的:

func inspect(_ value1: Any, _ value2: Any) {
   print("1. Received \(type(of: value1)) with the value \(value1)")
   print("2. Received \(type(of: value2)) with the value \(value2)")
}
inspect(42, "Dolphin")

范型限制(Limiting generics)

你常常希望限制泛型,以便它們只能對類似類型的數(shù)據(jù)進行操作,而 Swift 使這一點變得既簡單又容易。下一個函數(shù)將對任意兩個整數(shù)進行平方,不管它們是IntUIntInt64,等等:

func square<T: Integer>(_ value: T) -> T {
   return value * value
}

注意,我為返回值添加了一個占位符數(shù)據(jù)類型。在本例中,它意味著函數(shù)將返回與它接受的數(shù)據(jù)類型相同的值。

擴展square()以支持其他類型的數(shù)字(如雙精度和浮點數(shù))比較困難,因為沒有覆蓋所有內(nèi)置數(shù)字類型的協(xié)議。我們來創(chuàng)建一個:

protocol Numeric {
   static func *(lhs: Self, rhs: Self) -> Self
}

它不包含任何代碼,它只定義了一個名為Numeric的協(xié)議,并聲明任何符合該協(xié)議的東西都必須能夠自我相乘。我們想把這個協(xié)議應(yīng)用到 FloatDoubleInt ,所以在協(xié)議下面加上這三行:

extension Float: Numeric {}
extension Double: Numeric {}
extension Int: Numeric {}

有了這個新協(xié)議,你可以滿足任何你想要的:

func square<T: Numeric>(_ value: T) -> T {
   return value * value
}
square(42)
square(42.556)

創(chuàng)建泛型數(shù)據(jù)類型(Creating a generic data type)

既然你已經(jīng)掌握了泛型函數(shù),讓我們進一步了解完全泛型數(shù)據(jù)類型:我們將創(chuàng)建一個泛型結(jié)構(gòu)。在創(chuàng)建泛型數(shù)據(jù)類型時,需要將占位符數(shù)據(jù)類型聲明為結(jié)構(gòu)名稱的一部分,然后可以根據(jù)需要在每個屬性和方法中使用該占位符。

我們將要構(gòu)建的結(jié)構(gòu)名為 deque,這是一種常見的抽象數(shù)據(jù)類型,意思是“雙端隊列”。常規(guī)隊列是將東西添加到隊列末尾,然后從隊列前端刪除它們的隊列。deque 是一個隊列,你可以將內(nèi)容添加到開頭或結(jié)尾,也可以從開頭或結(jié)尾刪除內(nèi)容。我選擇在這里使用deque,因為重用 Swift 的內(nèi)置數(shù)組非常簡單——這里的關(guān)鍵是概念,而不是實現(xiàn)!

為了創(chuàng)建 deque 結(jié)構(gòu),我們將給它一個存儲數(shù)組屬性,它本身是通用的,因為它需要保存 deque 存儲的任何數(shù)據(jù)類型。我們還將添加四個方法: pushBack()pushFront() 將接受類型為T的參數(shù)并將其添加到正確的位置,而popBack()popFront()將返回一個T?(占位符可選數(shù)據(jù)類型),如果存在值,它將從后面或前面返回值。

只有一個很小的復(fù)雜性,那就是數(shù)組沒有返回T?popFirst()方法,因此我們需要添加一些額外的代碼,以便在數(shù)組為空時運行。這是代碼:

struct deque<T> {
   var array = [T]()
   mutating func pushBack(_ obj: T) {
      array.append(obj)
    }
   mutating func pushFront(_ obj: T) {
      array.insert(obj, at: 0)
   }
   mutating func popBack() -> T? {
      return array.popLast()
   }
   mutating func popFront() -> T? {
      if array.isEmpty {
         return nil
      } else {
         return array.removeFirst()
      }
  } 
}

有了這個結(jié)構(gòu),我們可以立即開始使用它:

var testDeque = deque<Int>()
testDeque.pushBack(5)
testDeque.pushFront(2)
testDeque.pushFront(1)
testDeque.popBack()

使用Cocoa類型(Working with Cocoa types)

Cocoa 數(shù)據(jù)類型—— NSArrayNSDictionary 等等——從 Swift 最早的版本開始就可以使用了,但是它們很難使用,因為 Objective-C 對泛型的支持是最近的,也是有限的。

NSCountedSet 是我最喜歡的基礎(chǔ)類型之一,它根本不支持泛型。這意味著你失去了 Swift 編譯器賦予你的自動類型安全,而這又讓你離 JavaScript 程序員更近了一步——你不想這樣吧? 當(dāng)然不。

幸運的是,我將向你演示如何通過圍繞 NSCountedSet 創(chuàng)建泛型包裝來創(chuàng)建自己的泛型數(shù)據(jù)類型。

這就像一個常規(guī)集合,每個條目只存儲一次,但是它還有一個額外的好處,那“你添加了 20 次數(shù)字 5 ”,盡管實際上它只在那里出現(xiàn)過一次。

這個的基本代碼并不難,盡管你需要導(dǎo)入 Foundation 來訪問NSCountedSet :

import Foundation
struct CustomCountedSet<T: Any> {

   let internalSet = NSCountedSet()

   mutating func add(_ obj: T) {
      internalSet.add(obj)
   }
   mutating func remove(_ obj: T) {
      internalSet.remove(obj)
   }
   func count(for obj: T) -> Int {
      return internalSet.count(for: obj)
   }
}

有了新的數(shù)據(jù)類型,你可以這樣使用它:

var countedSet = CustomCountedSet<String>()
countedSet.add("Hello")
countedSet.add("Hello")
countedSet.count(for: "Hello")
var countedSet2 = CustomCountedSet<Int>()
countedSet2.add(5)
countedSet2.count(for: 5)

我們的結(jié)構(gòu)體所做的就是包裝NSCountedSet使其類型安全,但這總是一個受歡迎的改進。考慮到蘋果在 Swift 3 中的發(fā)展方向,如果他們在未來將NSCountedSet重新實現(xiàn)為一個通用的基于結(jié)構(gòu)體的CountedSet,我不會感到驚訝——讓我們拭目以待!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,157評論 2 375

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