Advanced-Swift-Sample-Code
1. 介紹
<1>
Swift 和其他語言的相似之處
Swift 是什么樣的語言
術(shù)語
值,變量,引用,常量 - 只有指向關(guān)系被常量化了,而對象本身還是可變的。
<2>
另一件復(fù)雜的事情。如果我們的結(jié)構(gòu)體中包含有引用類型,在將結(jié)構(gòu)體賦值給一個新變量時所發(fā)生的復(fù)制行為中,這些引用類型的內(nèi)容是不會被自動復(fù)制一份的,只有引用本身會被復(fù)制。這種復(fù)制的行為被稱作淺復(fù)制 (shallow copy)。
舉個例子,F(xiàn)oundation 框架中的 Data 結(jié)構(gòu)體實(shí)際上是對引用類型 NSData 的一個封裝。不過, Data 的作者采取了額外的步驟,來保證當(dāng) Data 結(jié)構(gòu)體發(fā)生變化的時候?qū)ζ渲械?NSData 對象進(jìn)行深復(fù)制。他們使用一種名為 “寫時復(fù)制” (copy-on-write) 的技術(shù)來保證操作的高效,我們會在結(jié)構(gòu)體和類里詳細(xì)介紹這種機(jī)制?,F(xiàn)在我們需要重點(diǎn)知道的是,這種寫時復(fù)制的特性并不是直接具有的,它需要額外進(jìn)行實(shí)現(xiàn)。
Swift 中,像是數(shù)組這樣的集合類型也都是對引用類型的封裝,它們同樣使用了寫時復(fù)制的方式來在提供值語義的同時保持高效。
<3>
閉包的定義
柯里化函數(shù) 定義
靜態(tài)派發(fā)
動態(tài)派發(fā) - vtable
重寫,重載,多態(tài)
<4>
Swift 風(fēng)格指南 - Swift API 設(shè)計準(zhǔn)則
2. 內(nèi)建集合類型
<1> 數(shù)組
i. 數(shù)組和可變性 - Array 與 NSArray 的不同
Swift 中的數(shù)組是值類型(結(jié)構(gòu)體),OC中的是引用類型(對象)
ii. 數(shù)組和可選值 - 數(shù)組的基本操作
iii. 數(shù)組變形
Map
i. 使用函數(shù)將行為參數(shù)化 - 標(biāo)準(zhǔn)庫與自定義方法,使用函數(shù)參數(shù)。
ii. 可變和帶有狀態(tài)的閉包
iii. 閉包是指那些可以捕獲自身作用域之外的變量的函數(shù)。
accumulate
Filter
一般來說,你只應(yīng)該在需要所有結(jié)果時才去選擇使用 filter。
all(matching)
Reduce
flatMap
使用 forEach 進(jìn)行迭代
數(shù)組類型
切片 - ArraySlice
橋接
<2> 字典
可變性
有用的字典方法 - merge,frequencies,mapValues
Hashable 要求 - 字典其實(shí)是哈希表。
<3> Set
i. Set 也是通過哈希表實(shí)現(xiàn)的
ii. 如果你需要高效地測試某個元素是否存在于序列中并且元素的順序不重要時,使用集合是更好的選擇 (同樣的操作在數(shù)組中的復(fù)雜度是 O(n))。
iii. 另外,當(dāng)你需要保證序列中不出現(xiàn)重復(fù)元素時,也可以使用集合。
集合代數(shù) - subtracting,intersection,formUnion
IndexSet & CharacterSet : SetAlgebra
IndexSet 表示了一個由正整數(shù)組成的集合。當(dāng)然,你可以用 Set<Int> 來做這件事,但是 IndexSet 更加高效,因為它內(nèi)部使用了一組范圍列表進(jìn)行實(shí)現(xiàn)。(它會存儲連續(xù)的范圍)
CharacterSet 是一個高效的存儲 Unicode 碼點(diǎn) (code point) 的集合。它經(jīng)常被用來檢查一個特定字符串是否只包含某個字符子集 (比如字母數(shù)字 alphanumerics 或者數(shù)字 decimalDigits) 中的字符。
不過,和 IndexSet 有所不同,CharacterSet 并不是一個集合類型。 它的名字,CharacterSet,是從 Objective-C 導(dǎo)入時生成的,在 Swift 中它也并不兼容 Swift 的 Character 類型??赡?UnicodeScalarSet 會是更好的名字
unique()
<4> Range
Range & ClosedRange
不能對 Range 或者 ClosedRange 進(jìn)行迭代,可以檢查某個元素是否存在于范圍中
→ 只有半開范圍能表達(dá)空間隔(也就是下界和上界相等的情況,比如5..<5)。
→ 只有閉合范圍能包括其元素類型所能表達(dá)的最大值(比如0...Int.max)。而半開范圍則要求范圍上界是一個比自身所包含的最大值還要大 1 的值。
CountableRange
它的元素類型需要遵守 Strideable 協(xié)議 (以整數(shù)為步?)。
CountableRange & CountableClosedRange
Strideable 的約束使得 CountableRange 和 CountableClosedRange 遵守 RandomAccessCollection,于是我們就能夠?qū)λ鼈冞M(jìn)行迭代了。
| 半開范圍 | 閉合范圍 | |
|---|---|---|
| 元素滿足 Comparable | Range | ClosedRange |
| 元素滿足 Strideable | CountableRange | CountableClosedRange |
| (以整數(shù)為步長) (集合類型) |
partial range
部分范圍 (partial range) 指的是將 ... 或 ..< 作為前置或者后置運(yùn)算符來使用時所構(gòu)造的范圍。
RangeExpression
首先,它允許我們詢問某個元素是否被包括在該范圍中。
其次,給定一個集合類型,它能夠計算出表達(dá)式所指定的完整的 Range
范圍和按條件遵守協(xié)議
半開的 Range 和閉合的 ClosedRange 之間的差異應(yīng)該會一直存在。沒有一種方法將 ClosedRange 轉(zhuǎn)換為 Range。
3. 集合類型協(xié)議
<1> Sequence
protocol Sequence {
associatedtype Iterator: IteratorProtocol
func makeIterator() -> Iterator
*// ... *
}
Iterator
protocol IteratorProtocol {
associatedtype Element
mutating func next() -> Element?
}
// for 的實(shí)現(xiàn)
var iterator = someSequence.makeIterator()
while let element = iterator.next() {
doSomething(with: element)
}
// 自定義
struct ConstantIterator: IteratorProtocol {
typealias Element = Int
mutating func next() -> Int? {
return 1
}
}
// e.g.
struct FibsIterator: IteratorProtocol {
var state = (0, 1)
mutating func next() -> Int? {
let upcomingNumber = state.0
state = (state.1, state.0 + state.1)
return upcomingNumber
}
}
遵守 Collection 協(xié)議
struct PrefixIterator: IteratorProtocol {
let string: String
var offset: String.Index
init(string: String) {
self.string = string
offset = string.startIndex
}
mutating func next() -> Substring? {
guard offset < string.endIndex else {
return nil
}
offset = string.index(after: offset)
return string[..<offset]
}
}
struct PrefixSequence: Sequence {
let string: String
func makeIterator() -> PrefixIterator {
return PrefixIterator(string: string)
}
}
for prefix in PrefixSequence(string: "Hello") {
print(prefix)
}
PrefixSequence(string: "Hello").map { $0.uppercased() }
迭代器和值語義
基于函數(shù)的迭代器和序列
// 自定義的結(jié)構(gòu)體具有值語義,而使用 AnyIterator 定義的沒有
func fibsIterator() -> AnyIterator<Int> {
var state = (0, 1)
return AnyIterator {
let upcomingNumber = state.0
state = (state.1, state.0 + state.1)
return upcomingNumber
}
}
let fibsSequence = AnySequence(fibsIterator)
Array(fibsSequence.prefix(10)) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
這兩個 sequence 方法非常有用,它們經(jīng)常用來代替?zhèn)鹘y(tǒng)的 C ?格的循環(huán),特別是當(dāng)下標(biāo)的步?不遵守線性關(guān)系的時候。
sequence(first:next:)
sequence(state:next:)
let fibsSequence2 = sequence(state: (0, 1)) {
// 在這里編譯器需要一些類型推斷的協(xié)助
(state: inout (Int, Int)) -> Int? in
let upcomingNumber = state.0
state = (state.1, state.0 + state.1)
return upcomingNumber
}
Array(fibsSequence2.prefix(10)) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
無限序列
sequence 對于 next 閉包的使用是被延遲的。
對于序列和集合來說,重要區(qū)別就是序列可以是無限的,而集合則不行。
不穩(wěn)定序列
舉一個破壞性的可消耗序列的例子:
let standardIn = AnySequence {
return AnyIterator {
readLine()
}
}
let numberedStdIn = standardIn.enumerated()
// 延遲生成的 enumerate
for (i, line) in numberedStdIn {
print("\(i+1): \(line)")
}
序列和迭代器之間的關(guān)系
對于像斐波納契序列這樣的穩(wěn)定序列來說,它們需要獨(dú)立的遍歷狀態(tài),這就是迭代器所提供的 (當(dāng)然還需要遍歷的邏輯,不過這部分是序列的內(nèi)容)。
makeIterator 方法的目的就是創(chuàng)建這樣一個遍歷狀態(tài)。
子序列
protocol Sequence {
associatedtype Element
associatedtype Iterator: IteratorProtocol
where Iterator.Element == Element
associatedtype SubSequence
// ...
}
// → prefix 和 suffix — 獲取開頭或結(jié)尾n個元素
// → prefix(while:) — 從開頭開始當(dāng)滿足條件時,
// → dropFirst 和 dropLast — 返回移除掉前n個或后n個元素的子序列
// → drop(while:) — 移除元素,直到條件不再為真,然后返回剩余元素
// → split — 將一個序列在指定的分隔元素時截斷,返回子序列的的數(shù)組
extension Sequence where Element: Equatable, SubSequence: Sequence, SubSequence.Element == Element {
func headMirrorsTail(_ n: Int) -> Bool {
let head = prefix(n)
let tail = suffix(n).reversed()
return head.elementsEqual(tail)
}
}
[1,2,3,4,2,1].headMirrorsTail(2) // true
鏈表
/// 一個簡單的鏈表枚舉
enum List<Element> {
case end
indirect case node(Element, next: List<Element>)
}
// 在這里使用 indirect 關(guān)鍵字可以告訴編譯器這個枚舉值 node 應(yīng)該被看做引用。
// 但是值類型不能循環(huán)引用自身,否則編譯器就無法計算它的尺寸了。
// indirect 關(guān)鍵字允許一個枚舉成員能夠被當(dāng)作引用,這樣一來,它就能夠持有自己了。
<2> Collection
穩(wěn)定的序列
i. 在實(shí)現(xiàn) Collection 協(xié)議時,最難的部分在于選取一個合適的索引類型來表達(dá)集合類型中的位置。
ii. 除了 Array,Dictionary,Set,String 和它的各種方式以外, 另外還有 CountableRange 和 UnsafeBufferPointer 也是集合類型。Foundation - Data 和 IndexSet
自定義的集合類型
為隊列設(shè)計協(xié)議
遵守 Collection 協(xié)議
遵守 ExpressibleByArrayLiteral 協(xié)議
關(guān)聯(lián)類型
i. Iterator
集合類型中的默認(rèn)迭代器類型是 IndexingIterator<Self>,這個類型是一個很簡單的結(jié)構(gòu)體,它是對集合的封裝,并用集合本身的索引來迭代每個元素。
ii. SubSequence
iii. IndexDistance
一個有符號整數(shù)類型,代表了兩個索引之間的步數(shù)。
iv. Indices
集合的 indices 屬性的返回值類型。它代表對于集合的所有有效下標(biāo)的索引所組成的 集合,并以升序進(jìn)行排列。
默認(rèn)類型是 DefaultIndices<Self>。
Indices 的默認(rèn)類型是 DefaultIndices<Self>。和 Slice 一樣,它是對于原來的集合類型的簡單封裝,并包含起始和結(jié)束索引。它需要保持對原集合的引用,這樣才能夠?qū)λ饕M(jìn)行步進(jìn)。
如果你的索引是整數(shù)類型,你可以直接使用 CountableRange<Index>
索引
Index: Comparable
索引必須要有確定的順序。
字典的索引是 DictionaryIndex 類型,它是一個指向字典內(nèi)部存儲緩沖區(qū)的不透明值。事實(shí)上這個類型只是一個 Int 偏移值的封裝。
索引失效
索引步進(jìn)
collection.index(after: someIndex)
當(dāng)你實(shí)現(xiàn)你自己的索引類型時,請記住盡可能地避免持有集合類型的引用。
自定義集合索引
str.split(separator: " ")
為了提高性能,我們要構(gòu)建一個 Words 集合,它能夠讓我們不一次性地計算出所有單詞,而是可以用延遲加載的方式進(jìn)行迭代。
切片
list.dropFirst()
let firstDropped2 = words.suffix(from: onePastStart)
let firstDropped3 = words[onePastStart...]
Slice 是基于任意集合類型的一個輕量級封裝
切片與原集合共享索引
如果你在通過集合類型的 indices 進(jìn)行迭代時,修改了集合的內(nèi)容,那么 indices 所持有的任何對原來集合類型的強(qiáng)引用都會破壞寫時復(fù)制的性能優(yōu)化,因為這會造成不必要的復(fù)制操作。
如果集合的尺寸很大的話,這會對性能造成很大的影響。
Workaround: 要避免這件事情發(fā)生,你可以將 for 循環(huán)替換為 while 循環(huán),然后手動在每次迭代的時候增加 索引值,這樣你就不會用到 indices 屬性。當(dāng)你這么做的時候,要記住一定要從 collection.startIndex 開始進(jìn)行循環(huán),而不要把 0 作為開始。
泛型 PrefixIterator
<3> 專門的集合類型
// → BidirectionalCollection — “一個既支持前向又支持后向遍歷的集合?!?br> // → RandomAccessCollection — “一個支持高效隨機(jī)存取索引遍歷的集合?!?br> // → MutableCollection — “一個支持下標(biāo)賦值的集合?!?br> // → RangeReplaceableCollection — “一個支持將任意子范圍的元素用別的集合中的元素進(jìn)行替換的集合?!?/p>
BidirectionalCollection
index(before:)
RandomAccessCollection
(a) 以任意距離移動一個索引
(b) 測量任意兩個索引之間的距離,兩者都需要是 O(1) 時間常數(shù)的操作。
MutableCollection
單個元素的下標(biāo)訪問方法 subscript 現(xiàn)在必須提供一個 setter
RangeReplaceableCollection
(a) 一個空的初始化方法 — 在泛型函數(shù)中這很有用,因為它允許一個函數(shù)創(chuàng)建相同類型的新的空集合。
(b) replaceSubrange(_:with:)方法 — 它接受一個要替換的范圍以及一個用來進(jìn)行替換的集合。
// → append(:) 和 append(contentsOf:) — 將 endIndex..<endIndex(也就是說末尾的空范圍)替換為單個或多個新的元素。
// → remove(at:) 和 removeSubrange(:) — 將 i...i 或者 subrange 替換為空集合。
// → insert(at:) 和 insert(contentsOf:at:) — 將 i..<i (或者說在數(shù)組中某個位置的空范圍)替換為單個或多個新的元素。
// → removeAll — 將 startIndex..<endIndex 替換為空集合。
想要適應(yīng)嚴(yán)格的類型系統(tǒng),需要大量的練習(xí)
4. 可選值
哨崗值
函數(shù)都返回了一個 “魔法” 數(shù)來表示函數(shù)并沒有返回真實(shí)的值
通過枚舉解決魔法數(shù)的問題
enum Optional<Wrapped> {
case none
case some(Wrapped)
}
獲取關(guān)聯(lián)值的唯一方法是通過使用 switch 或者 if case 這樣的語句來進(jìn)行模式匹配。
可選值概覽
if let
while let
// 一旦你理解 for...in 其實(shí)是 while let
不過 do 代碼塊自身還有一個作用,就是引入一個新的作用域。
雙重可選值
因為 case 匹配可以通過重載 ~= 運(yùn)算符來進(jìn)行擴(kuò)展
if var && while var
解包后可選值的作用域
提前退出可以幫助我們在這個函數(shù)稍后的部分避免嵌套或者重復(fù)的檢查。
guard else
唯一的限制是你必須在 else 中離開當(dāng)前的作用域,也就是說,在代碼塊的最后你必須寫 return 或 者調(diào)用 fatalError (或者其他被聲明為返回 Never 的方法)。如果你是在循環(huán)中使用 guard 的話, 那么最后也可以是 break 或者 continue。
public enum Never { }
public typealias Void = ()
Swift 對 “東西不存在”(nil),“存在且為空”(Void) 以及“不可能發(fā)生” (Never) 這幾個概念進(jìn)行了仔細(xì)的區(qū)分。
最后 Swift 編譯器還會檢查你是否確實(shí)在 guard 塊中退出了當(dāng)前作用域,如果沒有的話,你會得到一個編譯錯誤。因為可以得到編譯器幫助,所以我們建議盡量選擇使用 guard,即便 if 也可以正常工作。
Optional chaining
可選鏈?zhǔn)且粋€ “展平” 操作。
可選鏈對下標(biāo)和函數(shù)調(diào)用也同樣適用
var optionalLisa: Person? = Person(name: "Lisa Simpson", age: 8)
optionalLisa?.age += 1
因為 Person 是一個結(jié)構(gòu)體,它是一個值類型,綁定后的值只是原來的值的局部作用域的復(fù)制,對這個復(fù)制進(jìn)行變更,并不會影響原來的值
如果 Person 是類的話,這么做是可行的。
var a: Int? = 5
a? = 10
a //Optional(10)
var b: Int? = nil
b? = 10
b //nil
??
在字符串插值中使用可選值
// 有時候你確實(shí)會想要在字符串插值中使用可選值,比如想要在調(diào)試的時候?qū)⑺闹荡蛴〕鰜?,在這種情況下,警告就很煩人了。
// 編譯器為我們提供了幾種修正這個警告的方式:
// <1> 顯式地用 as Any 進(jìn)行轉(zhuǎn)換,
// <2> 使用 ! 對值進(jìn)行強(qiáng)制解包 (如果你能確定該值不為 nil 時),
// <3> 使用 String(describing: ...) 對它進(jìn)行包裝,<4> 或者用 nil 合并運(yùn)算符提供一個默認(rèn)值。
Optional map
extension Optional {
func map<U>(transform: (Wrapped) -> U) -> U? {
if let value = self {
return transform(value)
}
return nil
}
}
Optional flatMap
如果你對一個可選值調(diào)用 map,但是你的轉(zhuǎn)換函數(shù)本身也返回可選值結(jié)果的話,最終結(jié)果將是一個雙重嵌套的可選值。
flatMap 可以把結(jié)果展平為單個可選值。
這說明 flatMap 和 if let 非常相似,可以相互重寫。
可選鏈也和 flatMap 很相似: i?.advance(by: 1) 實(shí)際上和 i.flatMap { $0.advance(by: 1) } 是等價的。
extension Optional {
func flatMap<U>(transform: (Wrapped) -> U?) -> U? {
if let value = self, let transformed = transform(value) {
return transformed
}
return nil
}
}
使用 flatMap 過濾 nil
numbers.flatMap { Int($0) }.reduce(0, +) // 6
我們之前已經(jīng)看過兩個 flatMap 了: 一個作用在數(shù)組上展平一個序列,另一個作用在可選值上展平可選值。
這里的 flatMap 是兩者的混合: 它將把一個映射為可選值的序列進(jìn)行展平。
func flatten<S: Sequence, T>(source: S) -> [T] where S.Element == T? {
let filtered = source.lazy.filter { $0 != nil }
return filtered.map { $0! }
}
extension Sequence {
func flatMap<U>(transform: (Element) -> U?) -> [U] {
return flatten(source: self.lazy.map(transform))
}
}
可選值判等
// 編譯器會幫助我們將值在需要時轉(zhuǎn)變?yōu)榭蛇x值。
if regex.first == "^" {
// 只匹配字符串開頭
}
func ==<T: Equatable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case (nil, nil): return true
case let (x?, y?): return x == y
case (_?, nil), (nil, _?): return false
}
}
Equatable 和 ==
func ==<T: Equatable>(lhs: [T?], rhs: [T?]) -> Bool {
return lhs.elementsEqual(rhs) { $0 == $1 }
}
可選值比較 如果你想要在可選值之間進(jìn)行除了相等之外的關(guān)系比較的話,現(xiàn)在你需要先對它們進(jìn)行解包, 然后明確地指出 nil 要如何處理
強(qiáng)制解包的時機(jī)
infix operator !!
改進(jìn)強(qiáng)制解包的錯誤信息
infix operator !?
在調(diào)試版本中進(jìn)行斷言
// 想要掛起一個操作我們有三種方式。
// <1> 首先,fatalError 將接受一條信息,并且無條件地停止操作。
// <2> 第二種選擇,使用 assert 來檢查條件,當(dāng)條件結(jié)果為 false 時,停止執(zhí)行并輸出信息。在發(fā)布版本中,assert 會被移除掉,條件不會被檢測,操作也永遠(yuǎn)不會掛起。
// <3> 第三種方式是使用 precondition,它和 assert 比較類型,但是在發(fā)布版本中它不會被移除,也就是說,只要條件被判定為 false,執(zhí)行就會被停止。
多災(zāi)多難的隱式解包可選值
原因 1: 暫時來說,你可能還需要到 Objective-C 里去調(diào)用那些沒有檢查返回是否存在的代碼
原因 2: 因為一個值只是很短暫地為 nil,在一段時間后,它就再也不會是 nil - outlet
隱式可選值行為
5. 結(jié)構(gòu)體和類
結(jié)構(gòu)體和類的主要不同點(diǎn):
// → 結(jié)構(gòu)體(和枚舉)是值類型,而類是引用類型。在設(shè)計結(jié)構(gòu)體時,我們可以要求編譯器保證不可變性。而對于類來說,我們就得自己來確保這件事情。
// → 內(nèi)存的管理方式有所不同。結(jié)構(gòu)體可以被直接持有及訪問,但是類的實(shí)例只能通過引用來間接地訪問。結(jié)構(gòu)體不會被引用,但是會被復(fù)制。也就是說,結(jié)構(gòu)體的持有者是唯一的,但是類的實(shí)例卻能有很多個持有者。
// → 使用類,我們可以通過繼承來共享代碼。而結(jié)構(gòu)體(以及枚舉)是不能被繼承的。想要在不同的結(jié)構(gòu)體或者枚舉之間共享代碼,我們需要使用不同的技術(shù),比如像是組合、泛型以及協(xié)議擴(kuò)展等。
值類型
<1> 不可變性也讓代碼天然地具有線程安全的特性,因為不能改變的東西是可以在線程之間安全地共享的。
<2> 改變一個結(jié)構(gòu)體變量的屬性,在概念上來說,和 為整個變量賦值一個全新的結(jié)構(gòu)體是等價的。
<3> 結(jié)構(gòu)體只有一個持有者。比如,當(dāng)我們將結(jié)構(gòu)體變量傳遞給一個函數(shù)時,函數(shù)將接收到結(jié)構(gòu)體的復(fù)制,它也只能改變它自己的這份復(fù)制。這叫做值語義 (value semantics),有時候也被叫做復(fù)制語義。而對于對象來說,它們是通過傳遞引用來工作的,因此類對象會擁有很多持有者, 這被叫做引用語義 (reference semantics)。
<4> 編譯器所做的對于值類型的復(fù)制優(yōu)化和值語義類型的寫時復(fù)制行為并不是一回事兒。寫時復(fù)制必須由開發(fā)者來實(shí)現(xiàn),想要實(shí)現(xiàn)寫時復(fù)制,你需要檢測所包含的類是否有共享的引用。
<5> 和自動移除不必要的值類型復(fù)制不同,寫時復(fù)制是需要自己實(shí)現(xiàn)的。不過編譯器會移除那些不必要的 “無效” 淺復(fù)制,以及像是 Array 這樣的類型中的代碼會執(zhí)行 “智能的” 寫時復(fù)制,兩者互為補(bǔ)充,都是對值類型的優(yōu)化。
可變性
class BinaryScanner 在 GCD 中訪問引發(fā)競態(tài)條件
結(jié)構(gòu)體
通過在擴(kuò)展中定義自定義方法,我們就可以同時保留原來的初始化方法
可變語義
對結(jié)構(gòu)體進(jìn)行改變,在語義上來說,與重新為它進(jìn)行賦值是相同的。
如果 Rectangle 是類的話,didSet 就不會被觸發(fā)了,因為在那種情況下,數(shù)組存儲的引用不會發(fā)生改變,只是引用指向的對象發(fā)生了改變。
可變方法
mutating
mutating 同時也是 willSet 和 didSet “知道” 合適進(jìn)行調(diào)用的依據(jù): 任何 mutating 方法的調(diào)用或者隱式的可變 setter 都會觸發(fā)這兩個事件。
在很多情況下,一個方法會同時有可變和不可變版本。比如數(shù)組有 sort() 方法 (這是個 mutating 方法,將在原地排序) 以及 sorted() 方法 (返回一個新的數(shù)組)。我們也可以為我們的 translate(by:_) 提供一個非 mutating 的版本。
extension Rectangle {
mutating func translate(by offset: Point) {
origin = origin + offset
}
}
screen.translate(by: Point(x: 10, y: 10))
screen // (10, 10, 320, 480)
extension Rectangle {
func translated(by offset: Point) -> Rectangle {
var copy = self
copy.translate(by: offset)
return copy
}
}
screen.translated(by: Point(x: 20, y: 20)) *// (30, 30, 320, 480) *
唯一會出現(xiàn)問題的地方是 你在不同的線程中引用了同一個全局或者被捕獲的的結(jié)構(gòu)體變量 (默認(rèn)情況下,閉包將被引用所捕獲)。
mutating 是如何工作的: inout 參數(shù)
<1> mutating 關(guān)鍵字做的正是此事。它可以將隱式的 self 參數(shù)變?yōu)榭勺兊摹?/p>
<2> 在全局函數(shù)中,我們可以將一個或多個參數(shù)標(biāo)記為 inout 來達(dá)到相同的效果。就和一個普通的參數(shù)一樣,值被復(fù)制并作為參數(shù)被傳到函數(shù)內(nèi)。不過,我們可以改變這個復(fù)制 (就好像它是被 var 定義的一樣)。然后當(dāng)函數(shù)返回時,Swift 會將這個 (可能改變過的) 值進(jìn)行復(fù)制并將其返回 給調(diào)用者,同時將原來的值覆蓋掉。
那些像是 += 這樣,可以對左側(cè)值進(jìn)行變更的運(yùn)算符,需要其參數(shù)為 inout。
使用值類型避免并行 bug
比如說,我們保持 BinaryScanner 是一個結(jié)構(gòu)體,但是我們將 scanRemainingBytes 方法的內(nèi)容內(nèi)聯(lián)使用的話, 我們就會和上面一樣面臨競態(tài)條件的問題。
寫時復(fù)制
<1> 這種行為就被稱為寫時復(fù)制。它的工作方式是,每當(dāng)數(shù)組被改變,它首先檢查它對存儲緩沖區(qū)的引用是否是唯一的,或者說,檢查數(shù)組本身是不是這塊緩沖區(qū)的唯一擁有者。
<2> 如果是,那么緩沖區(qū)可以進(jìn)行原地變更; 也不會有復(fù)制被進(jìn)行。
不過,如果緩沖區(qū)有一個以上的持有者 (如本例中),那么數(shù)組就需要先進(jìn)行復(fù)制,然后對復(fù)制的值進(jìn)行變化,而保持其他的持有者不受影響。
實(shí)現(xiàn)寫時復(fù)制
寫時復(fù)制 (昂貴方式)
寫時復(fù)制 (高效方式)
<1> 在 Swift 中,我們可以使用 isKnownUniquelyReferenced 函數(shù)來檢查某個引用只有一個持有者。
<2> 不過,對于 Objective-C 的類,它會直接返回 false。所以,直接對 NSMutableData 使用這個函數(shù)的話沒什么意義。我們可以創(chuàng)建一個簡單的 Swift 類,來將任意的 Objective-C 對象 (或者其他任意值) 封裝到 Swift 對象中
<3> 這項技術(shù)讓你能夠在創(chuàng)建保留值語義的結(jié)構(gòu)體的同時,保持像對象和指針那樣的高效操作。
寫時復(fù)制的陷阱
<0>
var array = [COWStruct()]
array[0].change() // No copy
var dict = ["key": COWStruct()]
dict["key"]?.change() // Optional("Copy")
<1> 如果在你將一個寫時復(fù)制的結(jié)構(gòu)體放到字典中,又想要避免這種復(fù)制的話,你可以將值用類封裝起來,這將為值賦予引用語義。
<2> 當(dāng)你在使用自己的結(jié)構(gòu)體時,也需要將這一點(diǎn)牢記于心。比如,我們可以創(chuàng)建一個儲存某個值的簡單地容器類型,通過直接訪問存儲的屬性,或者間接地使用下標(biāo),都可以訪問到這個值。當(dāng)我們直接訪問它的時候,我們可以獲取寫時復(fù)制的優(yōu)化,但是當(dāng)我們用下標(biāo)間接訪問的時候,復(fù)制會發(fā)生
<3> Array 通過使用地址器 (addressors) 的方式實(shí)現(xiàn)下標(biāo)。地址器允許對內(nèi)存進(jìn)行直接訪問。數(shù)組的下標(biāo)并不是返回元素,而是返回一個元素的地址器。這樣一來,元素的內(nèi)存可以被原地改變,而不需要再進(jìn)行不必要的復(fù)制。你可以在你自己的代碼中使用地址器,但是因為它們沒有被官方文檔化,所以也許會發(fā)生改變。要了解更多信息,可以看看 Swift 倉庫中關(guān)于 Accessors 的文檔。
閉包和可變性
如果我們傳遞這些閉包和函數(shù),它們會以引用的方式存在,并共享同樣的狀態(tài)。
func uniqueIntegerProvider() -> AnyIterator<Int> {
var i = 0
return AnyIterator {
i += 1
return i
}
}
Swift 的結(jié)構(gòu)體一般被存儲在棧上,而非堆上。不過對于可變結(jié)構(gòu)體,這其實(shí)是一種優(yōu)化:默認(rèn)情況下結(jié)構(gòu)體是存儲在堆上的,但是在絕大多數(shù)時候,這個優(yōu)化會生效,并將結(jié)構(gòu)體存儲到棧上。編譯器這么做是因為那些被逃逸閉包捕獲的變量需要在棧幀之外依然存在。當(dāng)編譯器偵測到結(jié)構(gòu)體變量被一個函數(shù)閉合的時候,優(yōu)化將不再生效,此時這個結(jié)構(gòu)體將存儲在堆上。這樣一來,在我們的例子里,就算 uniqueIntegerProvider 退出了作用域,i 也將繼續(xù)存在。
內(nèi)存
struct Person {
let name: String
var parents: [Person]
}
var john = Person(name: "John", parents: [])
john.parents = [john]
john // John, parents:[John, parents:[]]
因為值類型的特點(diǎn),當(dāng)你把 john 加到數(shù)組中的時候,其實(shí)它被復(fù)制了。更精確地說的話,應(yīng)該是 “你把 john 的值加到了數(shù)組中。” 要是 Person 是一個類的話,我們就會引入一個循環(huán)引用。 但是在這里的結(jié)構(gòu)體版本中,john 只有一個持有者,那就是原來的變量值 john。
引用循環(huán)
weak 引用
unowned 引用
<1> 對每個 unowned 的引用,Swift 運(yùn)行時將為這個對象維護(hù)另外一個引用計數(shù)。當(dāng)所有的 strong 引用消失時,對象將把它的資源 (比如對其他對象的引用) 釋放掉。不過,這個對象本身的內(nèi)存將繼續(xù)存在,直到所有的 unowned 引用也都消失。這部分內(nèi)存將被標(biāo)記為無效 (有時候我們也把它叫做僵尸 (zombie) 內(nèi)存),當(dāng)我們試圖訪問這樣的 unowned 引用時,就會發(fā)生運(yùn)行時錯誤。
在 unowned 和 weak 之間進(jìn)行選擇
<1> 從根本上來說,這個問題取決于相關(guān)對象的生命周期。如果這些對象的生命周期互不相關(guān),也就是說,你不能保證哪一個對象存在的時間會比另一個?,那么弱引用就是唯一的選擇。
<2> 另一種情況下,如果你可以保證非強(qiáng)引用對象擁有和強(qiáng)引用對象同樣或者更?的生命周期的話, unowned 引用通常會更方便一些。
這是因為我們可以不需要處理可選值,而且變量將可以被 let 聲明,而與之相對,弱引用必須被聲明為可選的 var。
<3> 主對象通過強(qiáng)引用控制子對象,子對象對主對象的逆向引用就可以是 unowned 引用。
<4> unowned 引用要比 weak 引用少一些性能損耗,因此訪問一個 unowned 引用的屬性或者調(diào)用它上面的方法都會稍微快一些; 不過,這個因素應(yīng)該只在性能非常重要的代碼路徑上才需要被考慮。
閉包和內(nèi)存
在 Swift 中,除了類以外,函數(shù) (包括閉包) 也是引用類型。我們在閉包和可變性的部分已經(jīng)看到過,閉包可以捕獲變量。如果這些變量自身是引用類型的話,閉包將持有對它們的強(qiáng)引用。
引用循環(huán)
我們可以通過使用捕獲列表 (capturelist) 來讓閉包不去引用視圖。
捕獲列表
捕獲列表也可以用來初始化新的變量。