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

第一章:語(yǔ)法(Syntax)

我最喜歡的 Swift 單行代碼是使用flatMap()來(lái)對(duì)一個(gè)數(shù)組進(jìn)行降維和過(guò)濾:

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

這行代碼看起來(lái)很簡(jiǎn)單,但它包含了很多很棒的 Swift 特性,如果你將其與 Objective-C 中最接近的開(kāi)箱即用的特性進(jìn)行比較,就會(huì)發(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é)過(guò)的東西,所以我就不在這里重復(fù)了。相反,我想關(guān)注一些有趣的初始化器,它們可能有助你更有效地使用常見(jiàn)的 Swift 類(lèi)型。

重復(fù)值(Repeating values)

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

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

Markdown 是一種很有用的格式,因?yàn)樗梢员挥?jì)算機(jī)解析,同時(shí)對(duì)人類(lèi)也具有視覺(jué)吸引力,而且下劃線為repeat:count: 提供了一個(gè)很好的例子。要使用這個(gè)初始化器,為其第一個(gè)參數(shù)指定一個(gè)字符串,并為其第二個(gè)參數(shù)指定重復(fù)的次數(shù),如下所示:

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

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

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

這個(gè)數(shù)組初始化器足夠靈活,你可以使用它非常容易地創(chuàng)建多維數(shù)組。例如,這創(chuàng)建了一個(gè)準(zhǔn)備填充 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)我看到這樣的代碼時(shí),我頭疼不已:

let str1 = "\(someInteger)"

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

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

let str2 = String(someInteger)

當(dāng)使用這種方式進(jìn)行轉(zhuǎn)換時(shí),事情會(huì)變得稍微困難一些,因?yàn)槟憧赡軙?huì)嘗試傳入一個(gè)無(wú)效的數(shù)字,例如:

let int1 = Int("elephant")

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

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

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

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

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

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

let str3 = String(28, radix: 16)

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

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

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

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

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

如果你有一個(gè)包含重復(fù)值的數(shù)組,并且希望找到一種快速刪除重復(fù)值的方法,則你需要的找的是Set。這是一個(gè)內(nèi)建的數(shù)據(jù)類(lèi)型,具有與普通數(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)

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

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

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

枚舉(Enums)

在模式匹配一章中,我已經(jīng)討論了枚舉關(guān)聯(lián)值,但這里我想重點(diǎn)討論枚舉本身,因?yàn)樗鼈兊墓δ芊浅?qiáng)大。

讓我們從一個(gè)非常簡(jiǎn)單的枚舉開(kāi)始,跟蹤一些基本的顏色:

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

如果你愿意,可以將所有case項(xiàng)寫(xiě)在一行上,如下所示:

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

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

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

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

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

初始值(Raw values)

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

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

只需添加:Int Swift 將為每種顏色都指定了一個(gè)匹配的整數(shù),從0 開(kāi)始向上計(jì)數(shù)。也就是說(shuō),unknown等于 0 ,blue等于 1 ,以此類(lèi)推。有時(shí),默認(rèn)值對(duì)你來(lái)說(shuō)并沒(méi)有用,所以如果需要,你可以為每個(gè)初始值指定單獨(dú)的整數(shù)。或者,你可以指定一個(gè)不同的起點(diǎn),使 Xcode 從那里開(kāi)始計(jì)數(shù)。

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

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

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

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

let marsNumber = Planet.mars.rawValue

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

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

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

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

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

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

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())")

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

枚舉沒(méi)有結(jié)構(gòu)體和類(lèi)那么強(qiáng)大,但是它們?cè)试S你在其中封裝一些有用的功能。例如,除非枚舉存儲(chǔ)的屬性是靜態(tài)的,否則不能給它們賦值,因?yàn)檫@樣做沒(méi)有意義,但是你可以添加在運(yùn)行一些代碼之后返回值的計(jì)算屬性。

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

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)然,計(jì)算屬性只是封裝方法的語(yǔ)法糖,所以你可以直接將方法添加到枚舉中也就不足為奇了?,F(xiàn)在讓我們通過(guò)向Color 枚舉添加兩個(gè)新方法來(lái)實(shí)現(xiàn)這一點(diǎn),forBoys()forGirls(),根據(jù)顏色來(lái)判斷一個(gè)玩具是為女孩還是男孩準(zhǔn)備的——只需在我們剛剛添加的description 屬性下面添加以下內(nèi)容:

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

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

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

數(shù)組(Arrays)

數(shù)組是 Swift 的真正主力之一。當(dāng)然,它們?cè)诖蠖鄶?shù)應(yīng)用程序中都很重要,但是它們對(duì)泛型的使用使它們?cè)谔砑右恍┯杏霉δ艿耐瑫r(shí)保證類(lèi)型安全。我不打算詳細(xì)介紹它們的基本用法;相反,我想向你介紹一些你可能不知道的有用方法。

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

為了演示下面的例子,我們將使用這兩個(gè)數(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()

一旦代碼運(yùn)行,sorted將包含["Tabitha", "Taylor", "Thomas", "Timothy", "Tobias", "Tyler"]。

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

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

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)位置排序,而不是返回一個(gè)新的排序數(shù)組。我還在其中添加了一個(gè)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

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

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

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

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

對(duì)于字符串,min()返回排序后的第一個(gè)字符串,max()返回最后一個(gè)字符串。如果你嘗試重用我為自定義排序提供的相同閉包,包括print()語(yǔ)句,你將看到min()max()實(shí)際上比使用sort()更高效,因?yàn)樗鼈儾恍枰苿?dòng)每一項(xiàng)。

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

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

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

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

為了便于測(cè)試,我們將創(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]

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

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

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

你會(huì)得到錯(cuò)誤,沒(méi)關(guān)系。

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

Dog結(jié)構(gòu)中添加這兩個(gè)函數(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
}

需要說(shuō)明的是,你的代碼應(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
   }
}

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

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

dogs.sort()

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

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

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

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

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

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

注意:removeLast()有一個(gè)稱(chēng)為removeFirst()的對(duì)應(yīng)項(xiàng),用于刪除和返回?cái)?shù)組中的初始項(xiàng)。遺憾的是,popLast()沒(méi)有類(lèi)似的方法。

空和容量(Emptiness and capacity)

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

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

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

警告:使用 reserveCapacity()不是一個(gè)免費(fèi)的操作。在后臺(tái),Swift 將創(chuàng)建一個(gè)包含相同值的新數(shù)組,并為你需要的容量留出空間。它不只是擴(kuò)展現(xiàn)有數(shù)組。這樣做的原因是該方法保證得到的數(shù)組將具有連續(xù)存儲(chǔ)(即所有項(xiàng)目彼此相鄰存儲(chǔ)而不是分散在 RAM 中)因此,Swift 會(huì)做大量的移動(dòng)操作。即使你已經(jīng)調(diào)用了reserveCapacity(),這也適用—嘗試將這段代碼放到一個(gè) 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)這段代碼運(yùn)行時(shí),你將看到調(diào)用reserveCapacity()時(shí)兩次都會(huì)出現(xiàn)嚴(yán)重的暫停。因?yàn)?code>reserveCapacity()是一個(gè)O(n)復(fù)雜度的調(diào)用(其中n是數(shù)組的count值),所以應(yīng)該在向數(shù)組添加項(xiàng)之前調(diào)用它。

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

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

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

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

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

就是這樣。連續(xù)數(shù)組具有你習(xí)慣使用的所有屬性和方法—count、sort()min()、map()等等—但因?yàn)樗许?xiàng)都保證是連續(xù)存儲(chǔ)的,即你可以得到更好的表現(xiàn)。

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

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

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

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)我運(yùn)行這段代碼時(shí),數(shù)組花費(fèi) 0.25 秒,連續(xù)數(shù)組花費(fèi) 0.13 秒??紤]到我們只是循環(huán)了超過(guò) 100 萬(wàn)個(gè)元素,這并不是非常優(yōu)秀,但如果你想在你的應(yīng)用程序或游戲中獲得額外的性能提升,你肯定應(yīng)該嘗試使用連續(xù)數(shù)組。

集合(Sets)

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

要將其置于上下文中,檢查數(shù)組是否包含項(xiàng),復(fù)雜度為O(n),這意味著“它取決于你在數(shù)組中有多少元素”。這是因?yàn)?code>Array.contains()需要從 0 開(kāi)始檢查每個(gè)元素,所以如果有 50 個(gè)元素,則需要執(zhí)行 50 次檢查。檢查一個(gè)集合是否包含項(xiàng),復(fù)雜度為O(1),這意味著“無(wú)論你有多少元素,它始終以相同的速度運(yùn)行”。這是因?yàn)榧系墓ぷ髟眍?lèi)似于字典:通過(guò)創(chuàng)建對(duì)象的 hash 生成鍵,而該鍵直接指向?qū)ο蟠鎯?chǔ)的位置。

基礎(chǔ)(The basics)

最好的實(shí)驗(yàn)方法是使用 Playground ,試著輸入這個(gè):

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

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

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

var set2 = Set(1...100)

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

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

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

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

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

set1.remove(3)

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

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

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

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

其次,集合的一些方法返回?cái)?shù)組而不是集合,因?yàn)檫@樣做更有用。例如,集合上的ordered()map()filter()方法返回一個(gè)數(shù)組。

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

for number in set1 {
   print(number)
}

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

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

像數(shù)組一樣,集合使用removeFirst()方法從集合的前面刪除項(xiàng)。 但是它的用途是不同的:因?yàn)榧鲜菬o(wú)序的,你真的不知道第一個(gè)項(xiàng)目是什么,所以removeFirst()實(shí)際上意味著“給我任何對(duì)象,以便我可以處理它?!?巧妙地,集合有一個(gè)popFirst()方法,而數(shù)組沒(méi)有——我真希望知道為什么!

集合操作(Set operations)

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

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

當(dāng)代碼運(yùn)行時(shí),union將包含 5 個(gè)條目,因?yàn)橹貜?fù)的 “Serenity” 只出現(xiàn)一次。

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

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

當(dāng)它運(yùn)行時(shí),intersection將包含Serenity,difference將包含NostromoEnterprise、VoyagerExecutor

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

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

這些方法是:

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

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

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

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 庫(kù)有一個(gè)專(zhuān)門(mén)的集合叫做NSCountedSet,它是一個(gè)具有扭曲(twist)的集合: 項(xiàng)仍然只能出現(xiàn)一次,但是如果你嘗試多次添加它們,它將跟蹤計(jì)數(shù),就像它們確實(shí)存在一樣。這意味著你可以獲得非重復(fù)集合的所有速度,但是如果允許重復(fù),你還可以計(jì)算項(xiàng)目出現(xiàn)的次數(shù)。

你可以根據(jù)需要從 Swift 數(shù)組或集合創(chuàng)建NSCountedSet。在下面的例子中,我創(chuàng)建了一個(gè)大型數(shù)組(帶有重復(fù)項(xiàng)),將它全部添加到計(jì)數(shù)集,然后打印出兩個(gè)值的計(jì)數(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:)來(lái)檢索一個(gè)元素在計(jì)數(shù)集合中出現(xiàn)的次數(shù)(理論上)。你可以使用countedSet.allObjects屬性提取所有對(duì)象的數(shù)組,但要注意:NSCountedSet不支持泛型,因此你需要將其類(lèi)型轉(zhuǎn)換回[String]。

元組(Tuples)

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

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

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

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

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

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

這段代碼完全正確:將一個(gè)整數(shù)賦值給一個(gè)單元素元組和將一個(gè)單元素元組賦值給一個(gè)整數(shù)都做了完全相同的事情。正如 Apple 文檔中所說(shuō),“如果括號(hào)中只有一個(gè)元素,那么(元組的)類(lèi)型就是該元素的類(lèi)型?!彼鼈儗?shí)際上是一樣的,所以你甚至可以這樣寫(xiě):

var singleTuple = (value: 42)
singleTuple = 69

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

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

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

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

元組有類(lèi)型(Tuples have types)

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

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

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

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

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

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)該——給你的元素命名,這樣你才能更明智地訪問(wèn)它們:

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

這些名稱(chēng)是類(lèi)型的一部分,所以這樣的代碼不會(huì)編譯通過(guò):

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

元組和閉包(Tuples and closures)

不能向元組添加方法,但可以添加閉包。我同意這種區(qū)別很好,但它很重要:向元組添加閉包就像添加任何其他值一樣,實(shí)際上是將代碼作為數(shù)據(jù)類(lèi)型附加到元組。因?yàn)樗皇且粋€(gè)方法,聲明有一點(diǎn)不同,但這里有一個(gè)例子讓你開(kāi)始:

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

singer.sing("Haters gonna hate")

注意:這些閉包不能訪問(wèn)同級(jí)元素,這意味著這樣的代碼不能工作:

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

返回多個(gè)值(Returning multiple values)

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

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

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

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

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

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

相比之下,如果 Swift 沒(méi)有元組,那么我們將不得不依賴(lài)于返回一個(gè)數(shù)組和按需要進(jìn)行類(lèi)型轉(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)

說(shuō)真的:如果inout是答案,你可能問(wèn)錯(cuò)了問(wèn)題。

可選元組(Optional tuples)

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

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

當(dāng)處理可選元組時(shí),Swift 不能使用類(lèi)型推斷,因?yàn)樵M中的每個(gè)元素都有自己的類(lèi)型。所以,你需要明確聲明你想要什么,就像這樣:

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

一般來(lái)說(shuō),可選元素很常見(jiàn),可選元組就不那么常見(jiàn)了。

比較元組(Comparing tuples)

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

例如,下面的代碼會(huì)打印“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)注類(lèi)型,這可能會(huì)產(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)看到了元組是多么強(qiáng)大、靈活和有用,但是有時(shí)候你可能想要將一些東西形式化。給你一個(gè)斯威夫特主題的例子,考慮這兩個(gè)元組,代表泰勒·斯威夫特的父母:

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

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

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

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” ,還讓妻子改成了她丈夫的姓,因?yàn)?Taylor Swift 的父母就是這么做的。很明顯,這只是一種婚姻形式,我希望你能理解這是一個(gè)簡(jiǎn)化的例子,而不是一個(gè)政治聲明。

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

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

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

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

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

范型(Generics)

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

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

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

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

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

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

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

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

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

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

稍后你將看到,占位符數(shù)據(jù)類(lèi)型也用于返回值。但是首先,這里是inspect()函數(shù)的最終版本,它輸出正確的信息,無(wú)論向它拋出什么數(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而不是某種類(lèi)型,這是一種常見(jiàn)的編碼約定:第一個(gè)占位符數(shù)據(jù)類(lèi)型名為T,第二個(gè)U和第三個(gè)V,以此類(lèi)推。在實(shí)踐中,我發(fā)現(xiàn)這個(gè)約定沒(méi)有幫助,也不清楚,所以盡管我將在這里使用它,只是因?yàn)槟惚仨毩?xí)慣它。

現(xiàn)在,你可能想知道泛型給這個(gè)函數(shù)帶來(lái)了什么好處——難道它就不能為它的參數(shù)類(lèi)型使用泛型嗎?在這種情況下可以,因?yàn)檎嘉环皇褂靡淮?,所以這在功能上是相同的:

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

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

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類(lèi)型的兩個(gè)參數(shù),這是占位符數(shù)據(jù)類(lèi)型。同樣,我們不知道這將是什么,這就是為什么我們給它一個(gè)抽象的名稱(chēng),如 “T ”,而不是一個(gè)特定的數(shù)據(jù)類(lèi)型,如IntString。然而,這兩個(gè)參數(shù)的類(lèi)型都是T,這意味著無(wú)論最終是什么類(lèi)型,它們都必須是相同的類(lèi)型。所以,這個(gè)代碼是合法的:

inspect(42, 42)

但這是行不通的,因?yàn)樗旌狭藬?shù)據(jù)類(lèi)型:

inspect(42, "Dolphin")

如果我們對(duì)數(shù)據(jù)類(lèi)型使用了Any參數(shù),那么 Swift 就不能確保兩個(gè)參數(shù)都是相同的類(lèi)型——一個(gè)可以是Int,另一個(gè)可以是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)

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

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

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

擴(kuò)展square()以支持其他類(lèi)型的數(shù)字(如雙精度和浮點(diǎn)數(shù))比較困難,因?yàn)闆](méi)有覆蓋所有內(nèi)置數(shù)字類(lèi)型的協(xié)議。我們來(lái)創(chuàng)建一個(gè):

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

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

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

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

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

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

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

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

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

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

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()
      }
  } 
}

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

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

使用Cocoa類(lèi)型(Working with Cocoa types)

Cocoa 數(shù)據(jù)類(lèi)型—— NSArray、NSDictionary 等等——從 Swift 最早的版本開(kāi)始就可以使用了,但是它們很難使用,因?yàn)?Objective-C 對(duì)泛型的支持是最近的,也是有限的。

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

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

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

這個(gè)的基本代碼并不難,盡管你需要導(dǎo)入 Foundation 來(lái)訪問(wèn)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ù)類(lèi)型,你可以這樣使用它:

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使其類(lèi)型安全,但這總是一個(gè)受歡迎的改進(jìn)??紤]到蘋(píng)果在 Swift 3 中的發(fā)展方向,如果他們?cè)谖磥?lái)將NSCountedSet重新實(shí)現(xiàn)為一個(gè)通用的基于結(jié)構(gòu)體的CountedSet,我不會(huì)感到驚訝——讓我們拭目以待!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容