UIKit框架(四十一) —— 使用協(xié)議構(gòu)建自定義Collection(一)

版本記錄

版本號 時(shí)間
V1.0 2020.06.27 星期六

前言

iOS中有關(guān)視圖控件用戶能看到的都在UIKit框架里面少梁,用戶交互也是通過UIKit進(jìn)行的。感興趣的參考上面幾篇文章凳鬓。
1. UIKit框架(一) —— UIKit動力學(xué)和移動效果(一)
2. UIKit框架(二) —— UIKit動力學(xué)和移動效果(二)
3. UIKit框架(三) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(一)
4. UIKit框架(四) —— UICollectionViewCell的擴(kuò)張效果的實(shí)現(xiàn)(二)
5. UIKit框架(五) —— 自定義控件:可重復(fù)使用的滑塊(一)
6. UIKit框架(六) —— 自定義控件:可重復(fù)使用的滑塊(二)
7. UIKit框架(七) —— 動態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(一)
8. UIKit框架(八) —— 動態(tài)尺寸UITableViewCell的實(shí)現(xiàn)(二)
9. UIKit框架(九) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(一)
10. UIKit框架(十) —— UICollectionView的數(shù)據(jù)異步預(yù)加載(二)
11. UIKit框架(十一) —— UICollectionView的重用魁亦、選擇和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用虚吟、選擇和重排序(二)
13. UIKit框架(十三) —— 如何創(chuàng)建自己的側(cè)滑式面板導(dǎo)航(一)
14. UIKit框架(十四) —— 如何創(chuàng)建自己的側(cè)滑式面板導(dǎo)航(二)
15. UIKit框架(十五) —— 基于自定義UICollectionViewLayout布局的簡單示例(一)
16. UIKit框架(十六) —— 基于自定義UICollectionViewLayout布局的簡單示例(二)
17. UIKit框架(十七) —— 基于自定義UICollectionViewLayout布局的簡單示例(三)
18. UIKit框架(十八) —— 基于CALayer屬性的一種3D邊欄動畫的實(shí)現(xiàn)(一)
19. UIKit框架(十九) —— 基于CALayer屬性的一種3D邊欄動畫的實(shí)現(xiàn)(二)
20. UIKit框架(二十) —— 基于UILabel跑馬燈類似效果的實(shí)現(xiàn)(一)
21. UIKit框架(二十一) —— UIStackView的使用(一)
22. UIKit框架(二十二) —— 基于UIPresentationController的自定義viewController的轉(zhuǎn)場和展示(一)
23. UIKit框架(二十三) —— 基于UIPresentationController的自定義viewController的轉(zhuǎn)場和展示(二)
24. UIKit框架(二十四) —— 基于UICollectionViews和Drag-Drop在兩個(gè)APP間的使用示例 (一)
25. UIKit框架(二十五) —— 基于UICollectionViews和Drag-Drop在兩個(gè)APP間的使用示例 (二)
26. UIKit框架(二十六) —— UICollectionView的自定義布局 (一)
27. UIKit框架(二十七) —— UICollectionView的自定義布局 (二)
28. UIKit框架(二十八) —— 一個(gè)UISplitViewController的簡單實(shí)用示例 (一)
29. UIKit框架(二十九) —— 一個(gè)UISplitViewController的簡單實(shí)用示例 (二)
30. UIKit框架(三十) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的簡單示例(一)
31. UIKit框架(三十一) —— 基于UICollectionViewCompositionalLayout API的UICollectionViews布局的簡單示例(二)
32. UIKit框架(三十二) —— 替換Peek and Pop交互的基于iOS13的Context Menus(一)
33. UIKit框架(三十三) —— 替換Peek and Pop交互的基于iOS13的Context Menus(二)
34. UIKit框架(三十四) —— Accessibility的使用(一)
35. UIKit框架(三十五) —— Accessibility的使用(二)
36. UIKit框架(三十六) —— UICollectionView UICollectionViewDiffableDataSource的使用(一)
37. UIKit框架(三十七) —— UICollectionView UICollectionViewDiffableDataSource的使用(二)
38. UIKit框架(三十八) —— 基于CollectionView轉(zhuǎn)盤效果的實(shí)現(xiàn)(一)
39. UIKit框架(三十九) —— iOS 13中UISearchController 和 UISearchBar的新更改(一)
40. UIKit框架(四十) —— iOS 13中UISearchController 和 UISearchBar的新更改(二)

開始

首先看下主要內(nèi)容:

本文主要講述了如何使用collection protocol來創(chuàng)建自己的Bag collection類型的實(shí)現(xiàn)。內(nèi)容來自翻譯怠肋。

下面是寫作環(huán)境:

Swift 5, iOS 13, Xcode 11

下面就是正文了

Array敬鬓,DictionarySet是Swift標(biāo)準(zhǔn)庫中捆綁在一起的常用集合類型。 但是,如果他們沒有立即提供您的應(yīng)用所需的一切钉答,該怎么辦础芍? 別擔(dān)心。 您可以使用Swift標(biāo)準(zhǔn)庫中的協(xié)議創(chuàng)建自己的自定義集合数尿!

Swift中的集合(Collections)帶有大量方便的實(shí)用程序仑性,可用于對它們進(jìn)行迭代,過濾和更多操作右蹦。 除了使用自定義集合之外诊杆,您還可以將所有業(yè)務(wù)邏輯添加到自己的代碼中。 但是何陆,這會使您的代碼腫晨汹,難以維護(hù)并且無法復(fù)制標(biāo)準(zhǔn)庫提供的內(nèi)容。

幸運(yùn)的是贷盲,Swift提供了強(qiáng)大的收集協(xié)議(collection protocol)淘这,因此您可以創(chuàng)建自己的收集類型,這些收集類型專門為滿足應(yīng)用程序的需求而量身定制巩剖。 您只需實(shí)現(xiàn)這些協(xié)議即可利用Swift集合的強(qiáng)大功能铝穷。

在本教程中,您將從頭開始構(gòu)建一個(gè)多集multiset(也稱為bag)球及。

在此過程中氧骤,您將學(xué)習(xí)如何:

  • 采用以下協(xié)議:Hashable呻疹,Sequence吃引,Collection,CustomStringConvertible刽锤,ExpressibleByArrayLiteral和ExpressibleByDictionaryLiteral镊尺。
  • 為您的集合創(chuàng)建自定義初始化。
  • 使用自定義方法改進(jìn)自定義集合并思。

是時(shí)候開始了庐氮!

注意:本教程適用于Swift 5.0。 由于對Swift標(biāo)準(zhǔn)庫進(jìn)行了重大更改宋彼,因此無法編譯以前的版本弄砍。

在起始文件夾中打開文件Bag.playground

注意:如果愿意输涕,可以創(chuàng)建自己的Xcode playground音婶。 如果這樣做,請刪除所有默認(rèn)代碼以從一個(gè)空的playground開始莱坎。


Creating the Bag Struct

接下來衣式,將以下代碼添加到您的playground

struct Bag<Element: Hashable> {
}

就這樣, “Papa’s got a brand new bag”

您的Bag是一種通用結(jié)構(gòu)碴卧,需要一個(gè)Hashable元素類型弱卡。 要求使用Hashable元素可以比較和存儲O(1)時(shí)間復(fù)雜度的唯一值。 這意味著住册,無論其內(nèi)容物的大小如何婶博,Bag都將以恒定的速度運(yùn)行。 另外荧飞,請注意凡蜻,您正在使用struct; 就像Swift對標(biāo)準(zhǔn)集合所做的那樣,這會強(qiáng)制執(zhí)行值語義悍募。

Bag就像Set溪厘,因?yàn)樗淮鎯χ貜?fù)的值。 所不同的是:Bag保留所有重復(fù)值的連續(xù)計(jì)數(shù)忠荞,而Set則不保留。

像購物清單一樣考慮它帅掘。 如果您想要多個(gè)委煤,則不要多次列出。 您只需在項(xiàng)目旁邊寫上您想要的號碼修档。

要對此建模碧绞,請將以下屬性添加到playground上的Bag中:

// 1
fileprivate var contents: [Element: Int] = [:]

// 2
var uniqueCount: Int {
  return contents.count
}

// 3
var totalCount: Int {
  return contents.values.reduce(0) { $0 + $1 }
}

這些是Bag所需的基本屬性。 這是每一步的工作:

  • 1) contents:使用Dictionary作為內(nèi)部數(shù)據(jù)結(jié)構(gòu)吱窝。 這對于Bag來說非常有用讥邻,因?yàn)樗鼤?qiáng)制執(zhí)行用于存儲元素的唯一鍵。 每個(gè)元素的值就是其計(jì)數(shù)院峡。 請注意兴使,您將此屬性標(biāo)記為fileprivate,以使Bag的內(nèi)部工作對外界隱藏照激。
  • 2) uniqueCount:返回唯一商品的數(shù)量发魄,忽略其單獨(dú)數(shù)量。 例如俩垃,一個(gè)包含4個(gè)橙子和3個(gè)蘋果的Bag將返回的uniqueCount2励幼。
  • 3) totalCount:返回Bag中的物品總數(shù)。 在上面的示例中口柳,totalCount將返回7苹粟。

Adding Edit Methods

現(xiàn)在,您將實(shí)現(xiàn)一些方法來編輯Bag的內(nèi)容啄清。

1. Adding Add Method

在剛添加的屬性下添加以下方法:

// 1
mutating func add(_ member: Element, occurrences: Int = 1) {
  // 2
  precondition(occurrences > 0,
    "Can only add a positive number of occurrences")

  // 3
  if let currentCount = contents[member] {
    contents[member] = currentCount + occurrences
  } else {
    contents[member] = occurrences
  }
}

這是這樣做的:

  • 1) add(_:occurrences :):提供一種向Bag添加元素的方法六水。它帶有兩個(gè)參數(shù):通用類型Element和一個(gè)可選的出現(xiàn)次數(shù)俺孙。您將方法標(biāo)記為mutating,因此可以修改contents實(shí)例變量掷贾。
  • 2) precondition(_:_ :):要求大于0次出現(xiàn)睛榄。如果此條件為假,則執(zhí)行停止想帅,并且遵循該條件的String將出現(xiàn)在playground調(diào)試器中场靴。
  • 3) 本部分檢查bag中是否已存在該元素。如果是這樣港准,它將增加計(jì)數(shù)旨剥。如果沒有,它將創(chuàng)建一個(gè)新元素浅缸。

注意:在本教程中轨帜,您將使用precondition,以確保按預(yù)期方式使用Bag衩椒。您還將使用precondition進(jìn)行健全性檢查蚌父,以確保在添加功能時(shí)一切正常。逐步執(zhí)行此操作將使您避免意外破壞以前運(yùn)行的功能毛萌。

現(xiàn)在您已經(jīng)可以將元素添加到Bag實(shí)例中苟弛,還需要一種將其刪除的方法。

2. Implementing the Remove Method

add(_:occurrences :)下面添加以下方法:

mutating func remove(_ member: Element, occurrences: Int = 1) {
  // 1
  guard 
    let currentCount = contents[member],
    currentCount >= occurrences 
    else {
      return
  }

  // 2
  precondition(occurrences > 0,
    "Can only remove a positive number of occurrences")

  // 3
  if currentCount > occurrences {
    contents[member] = currentCount - occurrences
  } else {
    contents.removeValue(forKey: member)
  }
}

請注意阁将,remove(_:occurrences :)add(_:occurrences :)具有相同的參數(shù)膏秫。 運(yùn)作方式如下:

  • 1) 首先,它檢查該元素是否存在做盅,并且至少具有調(diào)用者要?jiǎng)h除的出現(xiàn)次數(shù)缤削。 如果不是,則該方法返回言蛇。
  • 2) 接下來僻他,確保要?jiǎng)h除的出現(xiàn)次數(shù)大于0宵距。
  • 3) 最后腊尚,它檢查元素的當(dāng)前計(jì)數(shù)是否大于要?jiǎng)h除的出現(xiàn)次數(shù)。 如果更大满哪,則通過從當(dāng)前計(jì)數(shù)中減去要?jiǎng)h除的出現(xiàn)次數(shù)來設(shè)置元素的新計(jì)數(shù)婿斥。 如果不大,則currentCountoccurrences相等哨鸭,它將完全刪除該元素民宿。

目前Bag并沒有做太多事情。 您無法訪問其內(nèi)容像鸡,也無法使用任何有用的收集方法(如map活鹰,filter等)對您的收藏進(jìn)行操作。

但是,一切都不會丟失志群! Swift提供了使Bag成為合法集合所需的工具着绷。 您只需要遵循一些協(xié)議即可。


Adopting Protocols

Swift中锌云,協(xié)議定義了一組屬性和方法荠医,這些屬性和方法必須在采用它的對象中實(shí)現(xiàn)。 要采用協(xié)議桑涎,只需在classstruct的定義后添加一個(gè)冒號彬向,后跟您要采用的協(xié)議名稱即可。 聲明采用協(xié)議后攻冷,請?jiān)趯ο笊蠈?shí)現(xiàn)所需的變量和方法娃胆。 完成后,您的對象將符合協(xié)議等曼。

這是一個(gè)簡單的例子缕棵。 當(dāng)前,Bag對象在Playground的結(jié)果側(cè)欄中幾乎沒有顯示任何信息涉兽。

將以下代碼添加到playground的末尾(結(jié)構(gòu)體外部)以查看Bag的運(yùn)行情況:

var shoppingCart = Bag<String>()
shoppingCart.add("Banana")
shoppingCart.add("Orange", occurrences: 2)
shoppingCart.add("Banana")
shoppingCart.remove("Orange")

然后按Command-Shift-Enter執(zhí)行playground招驴。

這將創(chuàng)建一個(gè)帶有少量水果的Bag。 如果您查看playground調(diào)試器枷畏,則會看到對象類型别厘,但不包含任何內(nèi)容。


Adopting CustomStringConvertible

幸運(yùn)的是拥诡,Swift僅針對這種情況提供了CustomStringConvertible協(xié)議触趴! 在Bag的大括號后添加以下內(nèi)容:

extension Bag: CustomStringConvertible {
  var description: String {
    return String(describing: contents)
  }
}

符合CustomStringConvertible要求實(shí)現(xiàn)一個(gè)名為description的單個(gè)屬性。 此屬性返回特定實(shí)例的文本表示形式渴肉。

您將在這里放置創(chuàng)建代表數(shù)據(jù)的字符串所需的任何邏輯冗懦。 由于Dictionary符合CustomStringConvertible,因此您只需將description調(diào)用委托給contents仇祭。

Command-Shift-Enter再次運(yùn)行playground披蕉。

看一下shoppingCart的最新改進(jìn)的調(diào)試信息:

太棒了! 現(xiàn)在乌奇,在向Bag添加功能時(shí)没讲,您將可以驗(yàn)證其內(nèi)容。

很好礁苗! 您正在創(chuàng)建自己喜歡的強(qiáng)大集合類型的過程中爬凑。 接下來是初始化。


Creating Initializers

非常煩人的是试伙,您一次只能添加一個(gè)元素嘁信。 您應(yīng)該能夠通過傳遞要添加的對象集合來初始化Bag于样。

將以下代碼添加到playground的末尾(但請注意,這尚不能編譯):

let dataArray = ["Banana", "Orange", "Banana"]
let dataDictionary = ["Banana": 2, "Orange": 1]
let dataSet: Set = ["Banana", "Orange", "Banana"]

var arrayBag = Bag(dataArray)
precondition(arrayBag.contents == dataDictionary,
  "Expected arrayBag contents to match \(dataDictionary)")

var dictionaryBag = Bag(dataDictionary)
precondition(dictionaryBag.contents == dataDictionary,
  "Expected dictionaryBag contents to match \(dataDictionary)")

var setBag = Bag(dataSet)
precondition(setBag.contents == ["Banana": 1, "Orange": 1],
  "Expected setBag contents to match \(["Banana": 1, "Orange": 1])")

這就是您期望創(chuàng)建Bag的方式潘靖。 但是它不會編譯百宇,因?yàn)槟形炊x一個(gè)初始化器來接收其他集合。 您將使用泛型(generics)秘豹,而不是為每種類型顯式創(chuàng)建初始化方法携御。

Bag實(shí)現(xiàn)中的totalCount下方添加以下方法:

// 1
init() { }

// 2
init<S: Sequence>(_ sequence: S) where
  S.Iterator.Element == Element {
  for element in sequence {
    add(element)
  }
}

// 3
init<S: Sequence>(_ sequence: S) where
  S.Iterator.Element == (key: Element, value: Int) {
  for (element, count) in sequence {
    add(element, occurrences: count)
  }
}

這是您剛剛添加的內(nèi)容:

  • 1) 首先,您創(chuàng)建了一個(gè)空的初始化程序既绕。在定義其他init方法時(shí)啄刹,您需要添加此代碼。
  • 2) 接下來凄贩,添加了一個(gè)初始化程序誓军,該初始化程序接受符合Sequence協(xié)議的所有內(nèi)容,其中該序列的元素與Bag的元素相同疲扎。這涵蓋了數(shù)組Array和集合Set類型昵时。您遍歷序列傳遞的內(nèi)容,并一次添加一個(gè)元素椒丧。
  • 3) 此后壹甥,您添加了一個(gè)類似的初始化程序,但是它接受類型為(Element壶熏,Int)的元組句柠。字典就是一個(gè)例子。在這里棒假,您遍歷序列中的每個(gè)元素并添加指定的計(jì)數(shù)溯职。

再次按Command-Shift-Enter即可運(yùn)行playground。請注意帽哑,您之前添加在底部的代碼現(xiàn)在可以使用谜酒。

1. Initializing Collections

這些通用的初始化程序?yàn)?code>Bag對象啟用了更多種類的數(shù)據(jù)源。但是妻枕,它們確實(shí)需要您首先創(chuàng)建傳遞給初始化程序的集合僻族。

為了避免這種情況,Swift提供了兩種協(xié)議來啟用序列文字的初始化佳头。文字(Literals)為您提供了一種無需顯式創(chuàng)建對象即可寫數(shù)據(jù)的簡便方法鹰贵。

要看到這一點(diǎn),首先將以下代碼添加到您的playground的末尾:(注意:在添加所需的協(xié)議之前康嘉,這也會產(chǎn)生錯(cuò)誤。)

var arrayLiteralBag: Bag = ["Banana", "Orange", "Banana"]
precondition(arrayLiteralBag.contents == dataDictionary,
  "Expected arrayLiteralBag contents to match \(dataDictionary)")

var dictionaryLiteralBag: Bag = ["Banana": 2, "Orange": 1]
precondition(dictionaryLiteralBag.contents == dataDictionary,
  "Expected dictionaryLiteralBag contents to match \(dataDictionary)")

上面的代碼是使用ArrayDictionary文字而不是對象進(jìn)行初始化的示例籽前。

現(xiàn)在亭珍,要使它們起作用敷钾,請?jiān)?code>CustomStringConvertible擴(kuò)展下面添加以下兩個(gè)擴(kuò)展:

// 1
extension Bag: ExpressibleByArrayLiteral {
  init(arrayLiteral elements: Element...) {
    self.init(elements)
  }
}

// 2
extension Bag: ExpressibleByDictionaryLiteral {
  init(dictionaryLiteral elements: (Element, Int)...) {
    self.init(elements.map { (key: $0.0, value: $0.1) })
  }
}
  • 1) ExpressibleByArrayLiteral用于根據(jù)數(shù)組樣式文字創(chuàng)建Bag。 在這里肄梨,您可以使用之前創(chuàng)建的初始化程序阻荒,并傳入elements集合。
  • 2) ExpressibleByDictionaryLiteral的功能相同众羡,但對于字典樣式的文字而言侨赡。 該映射將元素轉(zhuǎn)換為初始化程序期望的命名元組。

Bag看起來更像是原生collection類型粱侣,是時(shí)候嘗試真正的魔術(shù)了羊壹。


Understanding Custom Collections

您現(xiàn)在已經(jīng)學(xué)到了足夠的知識,可以理解什么是自定義集合(collection):您定義的集合對象既符合Sequence協(xié)議又符合Collection協(xié)議齐婴。

在上一節(jié)中油猫,您定義了一個(gè)初始化程序,該初始化程序接受符合Sequence協(xié)議的集合對象柠偶。 Sequence表示一種類型情妖,該類型提供對其元素的順序,迭代訪問诱担。 您可以將序列視為一系列項(xiàng)目毡证,讓您一次遍歷每個(gè)元素。

There are way too many Pokemon to keep track these days

迭代是一個(gè)簡單的概念蔫仙,但是此功能為您的對象提供了巨大的功能情竹。它允許您執(zhí)行各種強(qiáng)大的操作,例如:

  • map(_ :):使用提供的閉包轉(zhuǎn)換序列中的每個(gè)元素后匀哄,返回結(jié)果數(shù)組秦效。
  • filter(_ :):返回滿足提供的閉包謂詞的元素?cái)?shù)組。
  • sorted(by :):返回基于提供的閉包謂詞排序的元素?cái)?shù)組涎嚼。

要查看Sequence中可用的所有方法阱州,請查看Apple’s documentation on the Sequence Protocol

1. Enforcing Non-destructive Iteration

一個(gè)警告:Sequence不需要符合性的類型是非破壞性的法梯。這意味著迭代后苔货,無法保證以后的迭代會從頭開始。如果您計(jì)劃多次迭代數(shù)據(jù)立哑,那將是一個(gè)巨大的問題夜惭。

要實(shí)施非破壞性迭代,您的對象需要符合Collection協(xié)議铛绰。

Collection繼承自IndexableSequence

主要區(qū)別在于诈茧,集合是可以多次遍歷并按索引訪問的序列。

遵循Collection捂掰,您將免費(fèi)獲得許多方法和屬性敢会。 一些例子是:

  • isEmpty:返回一個(gè)布爾值曾沈,指示集合是否為空。
  • first:返回集合中的第一個(gè)元素鸥昏。
  • count:返回集合中元素的數(shù)量塞俱。

根據(jù)集合中元素的類型,還有更多可用的方法吏垮。 在Apple的Apple’s documentation on the Collection Protocol中查看它們障涯。

抓住你的Bag,并采用這些協(xié)議膳汪!


Adopting the Sequence Protocol

對集合類型執(zhí)行的最常見操作是遍歷其元素唯蝶。 例如,將以下內(nèi)容添加到playground的末尾:

for element in shoppingCart {
  print(element)
}

ArrayDictionary一樣旅敷,您應(yīng)該能夠遍歷Bag生棍。 由于當(dāng)前的Bag類型不符合Sequence,因此無法編譯媳谁。

現(xiàn)在修復(fù)該問題涂滴。

1. Conforming to Sequence

ExpressibleByDictionaryLiteral擴(kuò)展之后添加以下內(nèi)容:

extension Bag: Sequence {
  // 1
  typealias Iterator = DictionaryIterator<Element, Int>

  // 2
  func makeIterator() -> Iterator {
    // 3
    return contents.makeIterator()
  }
}

并不需要太多符合Sequence。 在上面的代碼中晴音,您:

  • 1) 創(chuàng)建名為IteratorTypealias作為DictionaryIterator柔纵。 Sequence要求知道如何迭代序列。 DictionaryIteratorDictionary對象用來迭代其元素的類型锤躁。 您之所以使用這種類型搁料,是因?yàn)?code>Bag將其基礎(chǔ)數(shù)據(jù)存儲在Dictionary中。
  • 2) 將makeIterator()定義為返回Iterator的方法系羞,以逐步瀏覽序列中的每個(gè)元素郭计。
  • 3) 通過委派contents上的makeIterator()來返回迭代器,該內(nèi)容本身符合Sequence椒振。

這就是使Bag符合Sequence所需的全部昭伸!

您現(xiàn)在可以遍歷Bag的每個(gè)元素,并獲取每個(gè)對象的計(jì)數(shù)澎迎。 在上一個(gè)for-in循環(huán)之后庐杨,將以下內(nèi)容添加到playground的末尾:

for (element, count) in shoppingCart {
  print("Element: \(element), Count: \(count)")
}

Command-Shift-Enter運(yùn)行playground。 打開playground控制臺夹供,您將按順序看到元素的打印輸出及其數(shù)量灵份。

2. Viewing Benefits of Sequence

能夠遍歷一個(gè)Bag可以啟用Sequence實(shí)現(xiàn)的許多有用方法。 將以下內(nèi)容添加到playground的末端哮洽,以查看其中的一些操作:

// Find all elements with a count greater than 1
let moreThanOne = shoppingCart.filter { $0.1 > 1 }
moreThanOne
precondition(
  moreThanOne.first!.key == "Banana" && moreThanOne.first!.value == 2,
  "Expected moreThanOne contents to be [(\"Banana\", 2)]")

// Get an array of all elements without their counts
let itemList = shoppingCart.map { $0.0 }
itemList
precondition(
  itemList == ["Orange", "Banana"] ||
    itemList == ["Banana", "Orange"],
  "Expected itemList contents to be [\"Orange\", \"Banana\"] or [\"Banana\", \"Orange\"]")

// Get the total number of items in the bag
let numberOfItems = shoppingCart.reduce(0) { $0 + $1.1 }
numberOfItems
precondition(numberOfItems == 3,
  "Expected numberOfItems contents to be 3")

// Get a sorted array of elements by their count in descending order
let sorted = shoppingCart.sorted { $0.0 < $1.0 }
sorted
precondition(
  sorted.first!.key == "Banana" && moreThanOne.first!.value == 2,
  "Expected sorted contents to be [(\"Banana\", 2), (\"Orange\", 1)]")

Command-Shift-Enter鍵可以運(yùn)行playground填渠,并查看它們的運(yùn)行情況。

這些都是使用序列的有用方法 - 您實(shí)際上是免費(fèi)獲得的!

現(xiàn)在揭蜒,您可能對Bag的使用方式感到滿意横浑,但是這樣做的樂趣在哪里剔桨? 您絕對可以改善當(dāng)前的Sequence實(shí)現(xiàn)屉更。


Improving Sequence

當(dāng)前,您依靠Dictionary為您處理繁重的工作洒缀。 很好瑰谜,因?yàn)樗箘?chuàng)建自己的強(qiáng)大集合變得容易。 問題在于它為Bag用戶帶來了奇怪而令人困惑的情況树绩。 例如萨脑,Bag返回類型為DictionaryIterator的迭代器并不直觀。

但是饺饭,Swift再次來了渤早! Swift提供類型AnyIterator來隱藏底層的迭代器。

用以下內(nèi)容替換Sequence擴(kuò)展的實(shí)現(xiàn):

extension Bag: Sequence {
  // 1
  typealias Iterator = AnyIterator<(element: Element, count: Int)>

  func makeIterator() -> Iterator {
    // 2
    var iterator = contents.makeIterator()

    // 3
    return AnyIterator {
      return iterator.next()
    }
  }
}

在此修訂的Sequence擴(kuò)展中瘫俊,您:

  • 1) 將Iterator定義為符合AnyIterator鹊杖,而不是DictionaryIterator。 然后扛芽,像以前一樣骂蓖,創(chuàng)建makeIterator()返回一個(gè)Iterator
  • 2) 通過在contents上調(diào)用makeIterator()創(chuàng)建Iterator川尖。 下一步需要此變量登下。
  • 3) 將Iterator包裝在新的AnyIterator對象中,以轉(zhuǎn)發(fā)其next()方法叮喳。 next()方法是在迭代器上調(diào)用的方法被芳,用于獲取序列中的下一個(gè)對象。

Command-Shift-Enter運(yùn)行playground馍悟。 您會注意到幾個(gè)錯(cuò)誤:

以前畔濒,您在使用DictionaryIterator時(shí)使用了keyvalue的元組名稱。 您已經(jīng)從外界隱藏了DictionaryIterator赋朦,并將暴露的元組名稱重命名為elementcount篓冲。

要修復(fù)錯(cuò)誤,請分別將keyvalue替換為elementcount宠哄。 立即運(yùn)行playground壹将,您的precondition塊將像以前一樣通過。

現(xiàn)在沒有人會知道您只是在使用Dictionary為您辛苦工作毛嫉!

是時(shí)候?qū)⒛?code>Bag帶回家了诽俯。


Adopting the Collection Protocol

事不宜遲,這里是創(chuàng)建集合的真正內(nèi)容:集合(Collection)協(xié)議! 重申一下暴区,集合Collection是一個(gè)序列闯团,您可以按索引對其進(jìn)行訪問并多次遍歷。

要采用Collection仙粱,您需要提供以下詳細(xì)信息:

  • startIndex和endIndex:定義集合的邊界房交,并公開橫向的起點(diǎn)。
  • subscript (position:):允許使用索引訪問集合中的任何元素伐割。 此訪問應(yīng)以O(1)時(shí)間復(fù)雜度運(yùn)行候味。
  • index(after :):在傳入索引之后立即返回索引。

擁有有效的收藏集僅需四個(gè)細(xì)節(jié)隔心。

Sequence擴(kuò)展之后添加以下代碼:

extension Bag: Collection {
  // 1
  typealias Index = DictionaryIndex<Element, Int>

  // 2
  var startIndex: Index {
    return contents.startIndex
  }

  var endIndex: Index {
    return contents.endIndex
  }

  // 3
  subscript (position: Index) -> Iterator.Element {
    precondition(indices.contains(position), "out of bounds")
    let dictionaryElement = contents[position]
    return (element: dictionaryElement.key,
      count: dictionaryElement.value)
  }

  // 4
  func index(after i: Index) -> Index {
    return contents.index(after: i)
  }
}

這很簡單白群。 在這里,您:

  • 1) 將Collection中定義的Index類型聲明為DictionaryIndex硬霍。 您會將這些索引傳遞給內(nèi)容帜慢。
  • 2) 從contents返回開始和結(jié)束索引。
  • 3) 使用precondition來強(qiáng)制執(zhí)行有效索引唯卖。 您從該索引處的contents返回值作為新的元組粱玲。
  • 4) 返回在contents上調(diào)用的index(after :)的值。

通過簡單地添加這些屬性和方法耐床,您就創(chuàng)建了一個(gè)功能齊全的集合密幔!

1. Testing Your Collection

將以下代碼添加到playground的末尾以測試一些新功能:

// Get the first item in the bag
let firstItem = shoppingCart.first
precondition(
  (firstItem!.element == "Orange" && firstItem!.count == 1) ||
  (firstItem?.element == "Banana" && firstItem?.count == 2),
  "Expected first item of shopping cart to be (\"Orange\", 1) or (\"Banana\", 2)")

// Check if the bag is empty
let isEmpty = shoppingCart.isEmpty
precondition(isEmpty == false,
  "Expected shopping cart to not be empty")

// Get the number of unique items in the bag
let uniqueItems = shoppingCart.count
precondition(uniqueItems == 2,
  "Expected shoppingCart to have 2 unique items")

// Find the first item with an element of "Banana"
let bananaIndex = shoppingCart.indices.first { 
  shoppingCart[$0].element == "Banana"
}!
let banana = shoppingCart[bananaIndex]
precondition(banana.element == "Banana" && banana.count == 2,
  "Expected banana to have value (\"Banana\", 2)")

再次運(yùn)行playground。 太棒了撩轰!

提示一下您對所做的事情感到非常滿意的那一刻胯甩,但感覺到即將出現(xiàn)"but wait, you can do better"的評論……嗯,您是對的堪嫂! 你可以做得更好偎箫。 您的Bag中仍有一些Dictionary


Improving Collection

Bag又回到了太多的內(nèi)部運(yùn)作皆串。 Bag的用戶需要使用DictionaryIndex對象來訪問集合中的元素淹办。

您可以輕松解決此問題。 在Collection擴(kuò)展名后面添加以下內(nèi)容:

// 1
struct BagIndex<Element: Hashable> {
  // 2
  fileprivate let index: DictionaryIndex<Element, Int>

  // 3
  fileprivate init(
    _ dictionaryIndex: DictionaryIndex<Element, Int>) {
    self.index = dictionaryIndex
  }
}

在上面的代碼中恶复,您:

  • 1) 定義一個(gè)新的通用類型BagIndex怜森。 像Bag一樣,這需要可用于字典的Hashable泛型類型谤牡。
  • 2) 使該索引類型的基礎(chǔ)數(shù)據(jù)成為DictionaryIndex對象副硅。 BagIndex實(shí)際上只是一個(gè)包裝,將其真實(shí)索引對外界隱藏翅萤。
  • 3) 創(chuàng)建一個(gè)接受DictionaryIndex進(jìn)行存儲的初始化程序恐疲。

現(xiàn)在,您需要考慮以下事實(shí),即Collection要求Index具有可比性培己,以允許比較兩個(gè)索引來執(zhí)行操作碳蛋。 因此,BagIndex需要采用Comparable省咨。

BagIndex之后添加以下擴(kuò)展名:

extension BagIndex: Comparable {
  static func ==(lhs: BagIndex, rhs: BagIndex) -> Bool {
    return lhs.index == rhs.index
  }

  static func <(lhs: BagIndex, rhs: BagIndex) -> Bool {
    return lhs.index < rhs.index
  }
}

這里的邏輯很簡單肃弟; 您正在使用DictionaryIndex的等效方法返回正確的值。


Updating BagIndex

現(xiàn)在茸炒,您可以準(zhǔn)備將Bag更新為使用BagIndex愕乎。 將Collection擴(kuò)展替換為以下內(nèi)容:

extension Bag: Collection {
  // 1
  typealias Index = BagIndex<Element>

  var startIndex: Index {
    // 2.1
    return BagIndex(contents.startIndex)
  }

  var endIndex: Index {
    // 2.2
    return BagIndex(contents.endIndex)
  }

  subscript (position: Index) -> Iterator.Element {
    precondition((startIndex ..< endIndex).contains(position),
      "out of bounds")
    // 3
    let dictionaryElement = contents[position.index]
    return (element: dictionaryElement.key,
      count: dictionaryElement.value)
  }

  func index(after i: Index) -> Index {
    // 4
    return Index(contents.index(after: i.index))
  }
}

每個(gè)帶編號的注釋都表示更改阵苇。它們是:

  • 1) 將Index類型從DictionaryIndex替換為BagIndex壁公。
  • 2) 從startIndexendIndexcontents創(chuàng)建一個(gè)新的BagIndex
  • 3) 使用BagIndexindex屬性訪問contents并從中返回元素绅项。
  • 4) 使用BagIndex的屬性從內(nèi)容獲取DictionaryIndex值紊册,并使用該值創(chuàng)建一個(gè)新的BagIndex

就這些快耿!用戶回到對存儲數(shù)據(jù)的方式一無所知囊陡。您還可能會更好地控制索引對象。

在總結(jié)之前掀亥,還有一個(gè)更重要的主題需要討論撞反。通過添加基于索引的訪問,您現(xiàn)在可以為集合中的一系列值建立索引搪花。是時(shí)候讓您了解slice如何與集合一起工作了遏片。


Using Slices

slice是視圖集合中元素的子序列。它使您無需復(fù)制就可以對元素的特定子序列執(zhí)行操作撮竿。

slice存儲對創(chuàng)建它的基礎(chǔ)集合的引用吮便。slice與它們的基本集合共享索引,保留對開始和結(jié)束索引的引用以標(biāo)記子序列范圍幢踏。slice具有O(1)空間復(fù)雜度髓需,因?yàn)樗鼈冎苯右闷浠炯稀?/p>

要查看其工作原理,請將以下代碼添加到playground的末尾:

// 1
let fruitBasket = Bag(dictionaryLiteral:
  ("Apple", 5), ("Orange", 2), ("Pear", 3), ("Banana", 7))

// 2
let fruitSlice = fruitBasket.dropFirst()

// 3
if let fruitMinIndex = fruitSlice.indices.min(by:
  { fruitSlice[$0] > fruitSlice[$1] }) {
  // 4
  let basketElement = fruitBasket[fruitMinIndex]
  let sliceElement = fruitSlice[fruitMinIndex]
  precondition(basketElement == sliceElement,
    "Expected basketElement and sliceElement to be the same element")
}

再次運(yùn)行playground房蝉。

在上面的代碼中僚匆,您:

  • 1) 創(chuàng)建一個(gè)由四個(gè)不同水果組成的水果籃。
  • 2) 移除第一類水果搭幻。實(shí)際上咧擂,這只是在水果籃中創(chuàng)建一個(gè)新的slice視圖(不包括您刪除的第一個(gè)元素),而不是創(chuàng)建一個(gè)全新的Bag對象粗卜。您會在結(jié)果欄中注意到這里的類型為Slice <Bag <String >>屋确。
  • 3) 在剩余的水果中找到最少出現(xiàn)的水果的索引。
  • 4) 證明即使從slice計(jì)算索引,您也可以使用基礎(chǔ)集合和切片中的索引來檢索相同的元素攻臀。

注意:對于基于哈希的集合(例如DictionaryBag)焕数,切片似乎沒什么用,因?yàn)樗鼈兊捻樞蛭匆匀魏斡幸饬x的方式定義刨啸。另一方面堡赔,Array是集合類型的一個(gè)很好的例子,其中切片在執(zhí)行子序列操作中起著巨大的作用设联。

在本教程中善已,您學(xué)習(xí)了如何在Swift中創(chuàng)建自定義集合。您對 Sequence离例,Collection换团,CustomStringConvertibleExpressibleByArrayLiteral宫蛆,ExpressibleByDictionaryLiteral添加了一致性艘包,并創(chuàng)建了自己的索引類型。

如果您想查看或?yàn)楦暾?code>Bag實(shí)施做出貢獻(xiàn)耀盗,請查看Swift Algorithm Club implementation實(shí)施以及Foundation實(shí)施NSCountedSet想虎。

這些只是Swift提供的用于創(chuàng)建健壯和有用的集合類型的所有協(xié)議的一種體驗(yàn)。如果您想了解一些此處未涵蓋的內(nèi)容叛拷,請查看以下內(nèi)容:

您還可以查看有關(guān)Protocols in Swift的更多信息舌厨,并了解有關(guān)采用Swift標(biāo)準(zhǔn)庫中可用的通用協(xié)議adopting common protocols的更多信息。

后記

本篇主要講述了使用協(xié)議構(gòu)建自定義Collection忿薇,感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末裙椭,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子煌恢,更是在濱河造成了極大的恐慌骇陈,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件瑰抵,死亡現(xiàn)場離奇詭異你雌,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)二汛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進(jìn)店門婿崭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人肴颊,你說我怎么就攤上這事氓栈。” “怎么了婿着?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵授瘦,是天一觀的道長醋界。 經(jīng)常有香客問我,道長提完,這世上最難降的妖魔是什么形纺? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮徒欣,結(jié)果婚禮上逐样,老公的妹妹穿的比我還像新娘。我一直安慰自己打肝,他們只是感情好脂新,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著粗梭,像睡著了一般争便。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上楼吃,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天始花,我揣著相機(jī)與錄音,去河邊找鬼孩锡。 笑死,一個(gè)胖子當(dāng)著我的面吹牛亥贸,可吹牛的內(nèi)容都是我干的躬窜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼炕置,長吁一口氣:“原來是場噩夢啊……” “哼荣挨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起朴摊,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤默垄,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后甚纲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體口锭,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年介杆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鹃操。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,615評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡春哨,死狀恐怖荆隘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情赴背,我是刑警寧澤椰拒,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布晶渠,位于F島的核電站,受9級特大地震影響燃观,放射性物質(zhì)發(fā)生泄漏乱陡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一仪壮、第九天 我趴在偏房一處隱蔽的房頂上張望憨颠。 院中可真熱鬧,春花似錦积锅、人聲如沸爽彤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽适篙。三九已至,卻和暖如春箫爷,著一層夾襖步出監(jiān)牢的瞬間嚷节,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工虎锚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留硫痰,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓窜护,卻偏偏與公主長得像效斑,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子柱徙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,630評論 2 359

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