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

第一章:語法(Syntax)

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

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

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

重復(fù)值(Repeating values)

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

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

Markdown 是一種很有用的格式晕粪,因?yàn)樗梢员挥?jì)算機(jī)解析,同時(shí)對(duì)人類也具有視覺吸引力渐裸,而且下劃線為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)使用過它好幾次了魂迄,毫無疑問!

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

let str2 = String(someInteger)

當(dāng)使用這種方式進(jìn)行轉(zhuǎn)換時(shí),事情會(huì)變得稍微困難一些绽榛,因?yàn)槟憧赡軙?huì)嘗試傳入一個(gè)無效的數(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,即大寫——請(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ù)類型,具有與普通數(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)

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

枚舉(Enums)

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

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

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

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

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

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

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

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

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

初始值(Raw values)

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

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

只需添加:Int Swift 將為每種顏色都指定了一個(gè)匹配的整數(shù)碰煌,從0 開始向上計(jì)數(shù)舒岸。也就是說,unknown等于 0 芦圾,blue等于 1 蛾派,以此類推。有時(shí),默認(rèn)值對(duì)你來說并沒有用洪乍,所以如果需要眯杏,你可以為每個(gè)初始值指定單獨(dú)的整數(shù)】前模或者岂贩,你可以指定一個(gè)不同的起點(diǎn),使 Xcode 從那里開始計(jì)數(shù)巷波。

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

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

通過明確指定水星的值為 1Xcode 將從那里向上計(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)求無效的行星編號(hào)時(shí)豌研,我們可以從其初始值創(chuàng)建行星枚舉妹田,并使用空值合并運(yùn)算符提供合理的默認(rèn)值:

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

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

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

不管初始值的數(shù)據(jù)類型是什么阴孟,或者是否有初始值晌纫,當(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)

枚舉沒有結(jié)構(gòu)體和類那么強(qiáng)大,但是它們?cè)试S你在其中封裝一些有用的功能闯冷。例如砂心,除非枚舉存儲(chǔ)的屬性是靜態(tài)的,否則不能給它們賦值蛇耀,因?yàn)檫@樣做沒有意義辩诞,但是你可以添加在運(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ì)算屬性只是封裝方法的語法糖外永,所以你可以直接將方法添加到枚舉中也就不足為奇了。現(xiàn)在讓我們通過向Color 枚舉添加兩個(gè)新方法來實(shí)現(xiàn)這一點(diǎn)拧咳,forBoys()forGirls()伯顶,根據(jù)顏色來判斷一個(gè)玩具是為女孩還是男孩準(zhǔn)備的——只需在我們剛剛添加的description 屬性下面添加以下內(nèi)容:

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

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

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

數(shù)組(Arrays)

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

第一:排序养交。只要數(shù)組存儲(chǔ)的元素類型遵循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"]

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

例如捂寿,我們可以編寫一個(gè)字符串排序算法口四,它的行為與常規(guī)的字母排序相同,但它總是將名稱 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

如你所見更卒,隨著算法的發(fā)展,名稱可以顯示為 $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()語句,你將看到min()max()實(shí)際上比使用sort()更高效并扇,因?yàn)樗鼈儾恍枰苿?dòng)每一項(xiàng)去团。

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

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

更聰明的解決方案是實(shí)現(xiàn)Comparable協(xié)議鞋吉,這反過來要求你使用操作符重載。稍后我們將對(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)體沒有遵循 Comparable協(xié)議坷檩,所以我們沒有在dogs數(shù)組上獲得簡(jiǎn)單的sort()ordered()方法却音,我們只獲得了需要自定義閉包才能運(yùn)行的方法。

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

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

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

下一步是讓第一次嘗試它的人感到困惑的地方:你需要實(shí)現(xiàn)兩個(gè)新函數(shù)句灌,但是它們有一些不同尋常的名稱夷陋,在處理操作符重載時(shí)需要一點(diǎn)時(shí)間來適應(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
}

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

如果你以前沒有使用過運(yùn)算符重載,那么這些函數(shù)名是不常見的资昧,但是我希望你能夠確切地了解它們的作用: 當(dāng)你編寫dog1 < dog2時(shí)使用 < 函數(shù)酬土,當(dāng)你寫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)使用過數(shù)組的append()insert()remove(at:)方法叽唱,但我想確保你知道添加和刪除項(xiàng)的其他方法屈呕。

如果想將兩個(gè)數(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)涉及到刪除項(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è)稱為removeFirst()的對(duì)應(yīng)項(xiàng)嫩挤,用于刪除和返回?cái)?shù)組中的初始項(xiàng)害幅。遺憾的是,popLast()沒有類似的方法岂昭。

空和容量(Emptiness and capacity)

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

第一個(gè)是isEmpty以现,如果數(shù)組沒有添加任何項(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ǔ),確保你有足夠的空間來容納你的建議容量萎胰。

警告:使用 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īng)該知道這兩行代碼在功能上是相同的:

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

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

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

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

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

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

也就是說,我發(fā)現(xiàn)連續(xù)數(shù)組無論如何都比數(shù)組快聘裁,即使是使用Int這樣的基本類型雪营。舉個(gè)簡(jiǎn)單的例子,下面的代碼把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)我運(yùn)行這段代碼時(shí)衡便,數(shù)組花費(fèi) 0.25 秒献起,連續(xù)數(shù)組花費(fèi) 0.13 秒×蜕拢考慮到我們只是循環(huán)了超過 100 萬個(gè)元素谴餐,這并不是非常優(yōu)秀,但如果你想在你的應(yīng)用程序或游戲中獲得額外的性能提升呆抑,你肯定應(yīng)該嘗試使用連續(xù)數(shù)組岂嗓。

集合(Sets)

了解集合和數(shù)組之間的區(qū)別 – 并知道哪一個(gè)在何時(shí)是正確的選擇 - 是任何 Swift 開發(fā)人員工具箱中的一項(xiàng)重要技能。集合可以被認(rèn)為是無序數(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 開始檢查每個(gè)元素摩桶,所以如果有 50 個(gè)元素桥状,則需要執(zhí)行 50 次檢查。檢查一個(gè)集合是否包含項(xiàng)硝清,復(fù)雜度為O(1)辅斟,這意味著“無論你有多少元素,它始終以相同的速度運(yùn)行”芦拿。這是因?yàn)榧系墓ぷ髟眍愃朴谧值洌和ㄟ^創(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 }。就像我說的缓苛,集合是無序的芳撒,所以你可能會(huì)在 Xcode 窗口中看到一些不同的東西。

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

var set2 = Set(1...100)

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

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ù)組和集合都有接受另一種類型的構(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序的坛吁,你真的不知道第一個(gè)項(xiàng)目是什么劳殖,所以removeFirst()實(shí)際上意味著“給我任何對(duì)象,以便我可以處理它拨脉《咭觯” 巧妙地,集合有一個(gè)popFirst()方法玫膀,而數(shù)組沒有——我真希望知道為什么!

集合操作(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將包含Serenitydifference將包含Nostromo肋拔、Enterprise锈津、VoyagerExecutor

注意:union()凉蜂、intersection()symmetricDifference()都有直接修改集合的替代方法琼梆,可以通過向方法前添加form來調(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)格子集申尤,不同之處在于后者必須排除相同的集合。 也就是說衙耕,如果集合 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è)專門的集合叫做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:)來檢索一個(gè)元素在計(jì)數(shù)集合中出現(xiàn)的次數(shù)(理論上)心墅。你可以使用countedSet.allObjects屬性提取所有對(duì)象的數(shù)組酿矢,但要注意:NSCountedSet不支持泛型,因此你需要將其類型轉(zhuǎn)換回[String]怎燥。

元組(Tuples)

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

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

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

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

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

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

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

var singleTuple = (value: 42)
singleTuple = 69

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

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

如果你沒有從一個(gè)函數(shù)返回任何東西颊乘,你得到一個(gè)元組参淹,如果你從一個(gè)函數(shù)返回幾個(gè)值,你得到一個(gè)元組疲牵,如果你返回一個(gè)值承二,你實(shí)際上也得到一個(gè)元組榆鼠。我認(rèn)為可以肯定地說纲爸,不管您是否知道,您已經(jīng)是一個(gè)頻繁使用元組的用戶了!

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

元組有類型(Tuples have types)

元組很容易被認(rèn)為是數(shù)據(jù)的開放垃圾場(chǎng)鸵荠,但事實(shí)并非如此:它們是強(qiáng)類型的冕茅,就像 Swift 中的其他所有東西一樣。這意味著你不能改變一個(gè)元組的類型一旦它被創(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)

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

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

元組和閉包(Tuples and closures)

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

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

singer.sing("Haters gonna hate")

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

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

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

元組通常用于從一個(gè)函數(shù)返回多個(gè)值高氮。事實(shí)上慧妄,如果這是元組帶給我們的唯一東西,那么與其他語言(包括 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)然,你不必指定元素的名稱罪裹,但是這無疑是一種很好的實(shí)踐饱普,這樣其他開發(fā)人員就知道應(yīng)該期望什么。

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

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

相比之下套耕,如果 Swift 沒有元組,那么我們將不得不依賴于返回一個(gè)數(shù)組和按需要進(jìn)行類型轉(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是答案碾牌,你可能問錯(cuò)了問題康愤。

可選元組(Optional tuples)

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

具有可選元素的元組必須存在肴捉,但其可選元素可以為nil∈迨眨可選元組必須填充其所有元素每庆,或者是nil。具有可選元素的可選元組可能存在今穿,也可能不存在缤灵,并且其每個(gè)可選元素可能存在,也可能不存在蓝晒。

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

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

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

比較元組(Comparing tuples)

Swift 允許你比較最多擁有 6 個(gè)參數(shù)數(shù)量的元組晾嘶,只要它們具有相同的類型妓雾。這意味著您可以使用==比較包含最多 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)注類型楷拳,這可能會(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")

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

當(dāng)他們結(jié)婚時(shí)蛾狗,安德里亞·芬利變成了安德里亞·斯威夫特,他們成為了夫妻仪媒。我們可以寫一個(gè)簡(jiǎn)單的函數(shù)來表示這個(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ú)看起來足夠好兼耀,但是marryTaylorsParents()函數(shù)看起來相當(dāng)糟糕。一次又一次地重復(fù)(first: String, last: String)會(huì)使它很難閱讀求冷,也很難更改瘤运。

Swift 的解決方案很簡(jiǎn)單: typealias關(guān)鍵字。這并不是特定于元組的匠题,但在這里它無疑是最有用的:它允許你為類型附加一個(gè)替代名稱拯坟。例如,我們可以創(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í)間來適應(yīng)盖淡。在本章中塞琼,我將演示如何(以及為什么!)創(chuàng)建自己的泛型,從函數(shù)開始禁舷,然后是結(jié)構(gòu)體彪杉,最后是包裝 Foundation類型。

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

設(shè)想一個(gè)函數(shù),它被設(shè)計(jì)用來打印關(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í)際上……我們不需要。這顯然是非沉砹瑁枯燥的代碼谱轨,我們需要將其擴(kuò)展到浮點(diǎn)數(shù)、布爾值吠谢、數(shù)組土童、字典等等。有一種更智能的解決方案稱為泛型編程工坊,它允許我們編寫處理稍后指定類型的函數(shù)献汗。Swift 中的通用代碼使用尖括號(hào)<>敢订,所以它非常明顯!

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

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

注意SomeType的用法:在函數(shù)名后面的尖括號(hào)中罢吃,用于描述value參數(shù)楚午。尖括號(hào)里的第一個(gè)是最重要的,因?yàn)樗x了你的占位符數(shù)據(jù)類型:inspect<SomeType>()意味著“一個(gè)名為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而不是某種類型窟却,這是一種常見的編碼約定:第一個(gè)占位符數(shù)據(jù)類型名為T昼丑,第二個(gè)U和第三個(gè)V,以此類推夸赫。在實(shí)踐中菩帝,我發(fā)現(xiàn)這個(gè)約定沒有幫助,也不清楚茬腿,所以盡管我將在這里使用它呼奢,只是因?yàn)槟惚仨毩?xí)慣它。

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

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

但是悴品,如果我們希望函數(shù)接受相同類型的兩個(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類型的兩個(gè)參數(shù)苔严,這是占位符數(shù)據(jù)類型定枷。同樣,我們不知道這將是什么届氢,這就是為什么我們給它一個(gè)抽象的名稱欠窒,如 “T ”,而不是一個(gè)特定的數(shù)據(jù)類型悼沈,如IntString贱迟。然而姐扮,這兩個(gè)參數(shù)的類型都是T絮供,這意味著無論最終是什么類型衣吠,它們都必須是相同的類型。所以壤靶,這個(gè)代碼是合法的:

inspect(42, 42)

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

inspect(42, "Dolphin")

如果我們對(duì)數(shù)據(jù)類型使用了Any參數(shù),那么 Swift 就不能確保兩個(gè)參數(shù)都是相同的類型——一個(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ì)類似類型的數(shù)據(jù)進(jìn)行操作亚茬,而 Swift 使這一點(diǎn)變得既簡(jiǎn)單又容易。下一個(gè)函數(shù)將對(duì)任意兩個(gè)整數(shù)進(jìn)行平方浓恳,不管它們是Int刹缝、UIntInt64颈将,等等:

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

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

擴(kuò)展square()以支持其他類型的數(shù)字(如雙精度和浮點(diǎn)數(shù))比較困難,因?yàn)闆]有覆蓋所有內(nèi)置數(shù)字類型的協(xié)議死姚。我們來創(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ù)類型(Creating a generic data type)

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

我們將要構(gòu)建的結(jié)構(gòu)名為 deque蝠猬,這是一種常見的抽象數(shù)據(jù)類型切蟋,意思是“雙端隊(duì)列”。常規(guī)隊(duì)列是將東西添加到隊(duì)列末尾榆芦,然后從隊(duì)列前端刪除它們的隊(duì)列柄粹。deque 是一個(gè)隊(duì)列喘鸟,你可以將內(nèi)容添加到開頭或結(jié)尾,也可以從開頭或結(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ù)類型森爽。我們還將添加四個(gè)方法: pushBack()pushFront() 將接受類型為T的參數(shù)并將其添加到正確的位置恨豁,而popBack()popFront()將返回一個(gè)T?(占位符可選數(shù)據(jù)類型),如果存在值爬迟,它將從后面或前面返回值橘蜜。

只有一個(gè)很小的復(fù)雜性,那就是數(shù)組沒有返回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)凡涩,我們可以立即開始使用它:

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

使用Cocoa類型(Working with Cocoa types)

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

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

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

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

這個(gè)的基本代碼并不難嗜逻,盡管你需要導(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使其類型安全,但這總是一個(gè)受歡迎的改進(jìn)∧嫒眨考慮到蘋果在 Swift 3 中的發(fā)展方向嵌巷,如果他們?cè)谖磥韺?code>NSCountedSet重新實(shí)現(xiàn)為一個(gè)通用的基于結(jié)構(gòu)體的CountedSet,我不會(huì)感到驚訝——讓我們拭目以待!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末室抽,一起剝皮案震驚了整個(gè)濱河市搪哪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌狠半,老刑警劉巖噩死,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件颤难,死亡現(xiàn)場(chǎng)離奇詭異神年,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)行嗤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門已日,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人栅屏,你說我怎么就攤上這事飘千。” “怎么了栈雳?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵护奈,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我哥纫,道長(zhǎng)霉旗,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任蛀骇,我火速辦了婚禮厌秒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘擅憔。我一直安慰自己鸵闪,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布暑诸。 她就那樣靜靜地躺著蚌讼,像睡著了一般。 火紅的嫁衣襯著肌膚如雪个榕。 梳的紋絲不亂的頭發(fā)上篡石,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音笛洛,去河邊找鬼夏志。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的沟蔑。 我是一名探鬼主播湿诊,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼瘦材!你這毒婦竟也來了厅须?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤食棕,失蹤者是張志新(化名)和其女友劉穎朗和,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體簿晓,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡眶拉,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了憔儿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片忆植。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖谒臼,靈堂內(nèi)的尸體忽然破棺而出朝刊,到底是詐尸還是另有隱情,我是刑警寧澤蜈缤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布拾氓,位于F島的核電站,受9級(jí)特大地震影響底哥,放射性物質(zhì)發(fā)生泄漏咙鞍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一叠艳、第九天 我趴在偏房一處隱蔽的房頂上張望奶陈。 院中可真熱鬧,春花似錦附较、人聲如沸吃粒。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽徐勃。三九已至,卻和暖如春早像,著一層夾襖步出監(jiān)牢的瞬間僻肖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工卢鹦, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留臀脏,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像揉稚,于是被迫代替她去往敵國(guó)和親秒啦。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

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