Advanced-Swift-Sample-Code
6. 編碼和解碼
概覽
/// 某個類型可以將?身編碼為?種外部表示。
public protocol Encodable {
/// 將值編碼到給定的 encoder 中。
public func encode(to encoder: Encoder) throws
}
/// 某個類型可以從外部表示中解碼得到?身。
public protocol Decodable {
/// 通過從給定的 decoder 中解碼來創(chuàng)建新的實例例。
public init(from decoder: Decoder) throws
}
public typealias Codable = Decodable & Encodable
編碼器和解碼器的核心任務(wù)是管理那些用來存儲序列后的數(shù)據(jù)的容器的層次
自動遵循協(xié)議
struct Coordinate: Codable {
var latitude: Double
var longitude: Double
//不需要實現(xiàn)
}
struct Placemark: Codable {
var name: String
var coordinate: Coordinate
}
代碼生成和 “普通” 的默認實現(xiàn)形式上來說唯一的不同在于,默認實現(xiàn)是標準庫的一部分,而 Codable 代碼的合成是存在于編譯器中的。
Encoding
<1> JSONEncoder & PropertyListEncoder 對于滿足 Codable 的類型,它們也將自動適配 Cocoa 的 NSKeyedArchiver。
<2> 除了通過一個屬性來設(shè)定輸出格式 (帶有縮進的易讀格式和/或按詞典對鍵進行排序) 以外, JSONEncoder 還支持對于日期 (包括 ISO 8601 或者 Unix epoch 時間戳),Data 值 (比如 Base64 方式) 以及如何在發(fā)生異常時處理浮點值 (作為無限或是 not a number) 進行自定義。
<3> 事實上,JSONEncoder 甚至都沒有實現(xiàn) Encoder 協(xié)議。它只是一個叫做 _JSONEncoder 的私有類的封裝,這個類實現(xiàn)了 Encoder 協(xié)議,并且進行實際的編碼工作。這么設(shè)計的原因是,頂層的編碼器應(yīng)該提供的是完全不同的 API (或者說,提供一個方法來開始編碼的過程),而不是一個在編碼過程中用來傳遞給 codable 類型的 Encoder 對象。將這些任務(wù)清晰地分解開,意味著在任意給定的情景下,使用編碼器的一方只能訪問到適當?shù)?API。比如,因為公開的配置 API 只暴露在頂層編碼器的定義中,所以一個 codable 類型不能在編碼過程中重新對編碼器進行配置。
Decoding
但是 Swift 團隊還是決定增加 API 的明確性,避免產(chǎn)生歧義要比最大化精簡代碼要來得重要。
你可以查看 DecodingError 類型的文檔來確認你可能會遇到哪些錯誤。
編碼過程
當你開始編碼過程時,編碼器會調(diào)用正在被編碼的值上的 encode(to: Encoder) 方法,并將編碼器自身作為參數(shù)傳遞進去。接下來,值需要將自己以合適的格式編碼到編碼器中。
places.encode(to: self)
容器
顯然 Encoder 的核心功能就是提供一個編碼容器。容器是編碼器存儲的一種沙盒表現(xiàn)形式。通過為每個要編碼的值創(chuàng)建一個新的容器,編碼器能夠確保每個值都不會覆蓋彼此的數(shù)據(jù)。
容器有三種類型:
<1> 鍵容器(KeyedContainer)可以對鍵值對進行編碼。將鍵容器想像為一個特殊的字典, 到現(xiàn)在為止,這是最常?的容器。在基于鍵的編碼容器中,鍵是強類型的,這為我們提供了類型安全和自動補全的特性。 編碼器最終會在寫入目標格式 (比如 JSON) 時,將鍵轉(zhuǎn)換為字符串 (或者數(shù)字),不過這對開發(fā)者來說是隱藏的。自定義編碼方式的最簡單的辦法就是更改你的類型所提供的鍵。
<2> 無鍵容器(UnkeyedContainer)將對一系列值進行編碼,而不需要對應(yīng)的鍵,可以將它想像成被編碼值的數(shù)組。因為沒有對應(yīng)的鍵來確定某個值,所以對于在容器中的值進行解碼的時候,需要遵守和編碼時同樣的順序。
<3> 單值容器對一個單一值進行編碼。你可以用它來處理整個數(shù)據(jù)被定義為單個屬性的那類類型。單值容器應(yīng)用的例子包括像是 Int 這樣的原始類型,或者是底層由原始類型的 RawRepresentable 所表達的枚舉值類型。
對于這三種容器,它們每個都對應(yīng)了一個協(xié)議,來定義容器應(yīng)當如何接收一個值并進行編碼。
SingleValueEncodingContainer & UnkeyedEncodingContainer & KeyedEncodingContainerProtocol
其他不屬于基礎(chǔ)類型的值,最后都會落到泛型的 encode<T: Encodable> 重載中。在這個方法里,容器最終會調(diào)用參數(shù)的 encode(to: Encoder) 方法,整個過程會向下一個層級并重新開始,最終到達只剩下原始類型的情況。不過容器在處理具體類型時,可以有自身的不同的特殊要求。
值是如何對自己編碼的
Array<Placemark> - 數(shù)組將會向編碼器請求一個無鍵容器
編譯器生成的代碼
Coding Keys
<1> 這個枚舉包含的成員與結(jié)構(gòu)體中的存儲屬性一一對應(yīng)。枚舉值即為鍵編碼容器所使用的鍵。
和字符串的鍵相比較,因為有編譯器檢查拼寫錯誤,所以這些強類型的鍵要更加安全,也更加方便。不過,編碼器最后為了存儲需要,還是必須要能將這些鍵轉(zhuǎn)為字符串或者整數(shù)值。
private enum CodingKeys: CodingKey {
case name
case coordinate
}
<2> CodingKey 協(xié)議會負責這個轉(zhuǎn)換任務(wù):
/// 該類型作為編碼和解碼時使?的鍵
public protocol CodingKey {
/// 在?個命名集合 (?如一個字符串作為鍵的字典) 中的字符串值。
var stringValue: String { get }
/// 在?個整數(shù)索引集合 (?如?個整數(shù)作為鍵的字典) 中使?的值。
var intValue: Int? { get }
init?(stringValue: String)
init?(intValue: Int)
}
// encode(to:) 方法
struct Placemark3: Codable {
// ...
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(coordinate, forKey: .coordinate)
}
}
// init(from:) 初始化方法
struct Placemark: Codable {
// ...
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
coordinate = try container.decode(Coordinate.self, forKey: .coordinate)
}
}
手動遵守協(xié)議
自定義 Coding Keys
<1> 使用明確給定的字符串值,在編碼后的輸出中重命名字段
<2> 將某個鍵從枚舉中移除,以此完全跳過字段。
struct Placemark3: Codable {
var name: String
var coordinate: Coordinate
private enum CodingKeys: String, CodingKey {
case name = "label"
case coordinate
}
// 編譯器??成的編碼和解碼?法將使用重載后的 CodingKeys
}
struct Placemark4: Codable {
var name: String = "(Unknown)" // 默認值
var coordinate: Coordinate
private enum CodingKeys: CodingKey {
case coordinate
}
}
自定義的 encode(to:) 和 init(from:) 實現(xiàn)
JSONEncoder 和 JSONDecoder 默認就可以處理可選值。如果目標類型中的一個屬性是可選值,那么輸入數(shù)據(jù)中對應(yīng)的值不存在的話,解碼器將會正確地跳過這個屬性。
常見的編碼任務(wù)
讓其他人的代碼滿足 Codable
<1> Swift 4.0 只會對那些在類型被定義的同時,就指定滿足 Codable 的類型生成代碼。
<2> 嵌套容器
KeyedDecodingContainer 有一個叫做 nestedContainer(keyedBy:forKey:) 的方法,它能夠 (使用另一套編碼鍵類型) 另外創(chuàng)建一個鍵容器。
<3> 使用計算屬性繞開問題
不過我們給用戶暴露一個 CLLocationCoordinate2D 的計算屬性。
由于它是一個計算屬性, Codable 系統(tǒng)會將它忽略掉。
讓類滿足 Codable
<1> 要讓這個動態(tài)派發(fā)正確工作,編譯器需要在類的派發(fā)表中為該初始化方法創(chuàng)建一個條目。該類的非 final 方法所對應(yīng)的表是在類的定義被編譯的時候進行創(chuàng)建的,在創(chuàng)建的時候它的大小就固定了;擴展不能再向其中添加新的條目。這就是為什么 required 初始化方法只能在類定義中存在的原因。
<2> 在 Swift 4 中,我們不能為一個非 final 的類添加 Codable 特性。
<3> 推薦的方式是寫一個結(jié)構(gòu)體來封裝 UIColor,并且對這個結(jié)構(gòu)體進行編解碼。 - 顏色空間沒有包含
<4> 封裝結(jié)構(gòu)體的方式最大的缺點在于,你需要手動在編碼前和解碼后將類型在 UIColor 和封裝類型之間進行轉(zhuǎn)換。
<5> 計算屬性
讓枚舉滿足 Codable
<1> 當枚舉類型滿足 RawRepresentable 協(xié)議,并且 RawValue 類型是 “原生” 的幾個 codable 類型 (也就是說,Bool,String,F(xiàn)loat,Double 或者任一整數(shù)類型) 時,編譯器會為枚舉類型提供 Codable 的代碼生成。
<2> 對于其他像是帶有關(guān)聯(lián)值的枚舉,你需要手動進行實現(xiàn)才能滿足 Codable - enum Either<A: Codable, B: Codable>: Codable
解碼多態(tài)集合
怎么才能對這樣的多態(tài)對象集合進行編碼呢?最好的方式是對每個我們想要支持的子類創(chuàng)建一個枚舉成員。枚舉的關(guān)聯(lián)值中存儲的是實際的對象
→ 在編碼過程中,對要編碼的對象在所有枚舉成員上做switch來找到我們要編碼的類型。 然后將對象的類型和對象本身編碼到它們的鍵中。
→ 在解碼過程中,先解碼類型信息,然后根據(jù)具體的類型選擇合適的初始化方法。
7. 函數(shù)
<1> 函數(shù)可以被賦值給變量,也能夠作為函數(shù)的輸入和輸出
<2> 函數(shù)能夠捕獲存在于其局部作用域之外的變量。
<3> 有兩種方法可以創(chuàng)建函數(shù),一種是使用 func 關(guān)鍵字,另一種是 {}。在Swift中,后一種被稱為閉包表達式。
在編程術(shù)語里,一個函數(shù)和它所捕獲的變量環(huán)境組合起來被稱為閉包。
使用閉包表達式來定義的函數(shù)可以被想成函數(shù)的字面量,與 func 相比較,它的區(qū)別在于閉包表達式是匿名的,它們沒有被賦予一個名字。
extension BinaryInteger {
var isEven: Bool { return self % 2 == 0 }
}
func isEven<T: BinaryInteger>(_ i: T) -> Bool {
return i % 2 == 0
}
記住,閉包指的是一個函數(shù)以及被它所捕獲的所有變量的組合。而使用 { } 來創(chuàng)建的函數(shù)被稱為閉包表達式,人們常常會把這種語法簡單地叫做閉包。
函數(shù)的靈活性
實際上一共有四個排序的方法:不可變版本的 sorted(by:) 和可變的 sort(by:),以及兩者在待排序?qū)ο笞袷?Comparable 時進行升序排序的無參數(shù)版本
排序描述符用到了 Objective-C 的兩個運行時特性:
<1> 首先,key 是 Objective-C 的鍵路徑,它其實是一個包含屬性名字的鏈表。不要把它和 Swift 4 引入的原生的 (強類型的) 鍵路徑搞混。
<2> 第二個 Objective-C 運行時特性是鍵值編程 (key-value-coding),它可以在運行時通過鍵查找一個對象上的對應(yīng)值。selector 參數(shù)接受一個 selector (實際上也是一個用來描述方法名字的字符串),在運行時,這個 selector 將被用來查找比較函數(shù),當對兩個對象進行比較時,這個比較函數(shù)將被用來對指定鍵所對應(yīng)的值進行比較。
函數(shù)作為數(shù)據(jù)
<1> 實現(xiàn) SortDescriptor
這種方式的實質(zhì)是將函數(shù)用作數(shù)據(jù),我們將這些函數(shù)存儲在數(shù)組里,并在運行時構(gòu)建這個數(shù)組。這將動態(tài)特性帶到了一個新的高度,這也是像 Swift 這樣的編譯時就確定了靜態(tài)類型的語言仍然能實現(xiàn)像是 Objective-C 或者 Ruby 的部分動態(tài)行為的一種方式。
<2> 自定義的運算符,來合并兩個排序函數(shù)
<3> 處理可選值
func lift<A>(_ compare: @escaping (A) -> (A) -> ComparisonResult) -> (A?) -> (A?) -> ComparisonResult
<4> 這樣的做法也讓我們能夠清晰地區(qū)分排序方法和比較方法的不同。Swift 的排序算法使用的是多個排序算法的混合。
在寫這本書的時候,排序算法基于的是內(nèi)省排序 (introsort),而內(nèi)省排序本身其實是快速排序和堆排序的混合。但是,當集合很小的時候,會轉(zhuǎn)變?yōu)椴迦肱判?insertion sort),以避免那些更復雜的排序算法所需要的顯著的啟動消耗。
局部函數(shù)和變量捕獲
merge 排序
函數(shù)作為代理
Foundation 框架的代理
結(jié)構(gòu)體代理
當我們給 alert.delegate 賦值的時候,Swift 將結(jié)構(gòu)體進行了復制。
在代理和協(xié)議的模式中,并不適合使用結(jié)構(gòu)體。
使用函數(shù),而非代理
<1> 函數(shù)類型只能有一個明確寫出的空類型標簽,配合上一個內(nèi)部的參數(shù)名字,而不能擁有獨立的參數(shù)標簽。它能讓我們給函數(shù)類型的參數(shù)一個標簽,用來作為進行文檔說明。在 Swift 支持一種更好的方式之前,這是官方所認可的變通方式。
<2> 函數(shù)回調(diào) - struct
<3> 函數(shù)回調(diào) - class - 循環(huán)引用
<4> 要注銷一個代理或者函數(shù)回調(diào),我們可以簡單地將它設(shè)為 nil。但如果我們的類型是用一個數(shù)組來存儲代理或者回調(diào)呢?
對于基于類的代理,我們可以直接將它從代理列表中移除; 不過對于回調(diào)函數(shù),就沒那么簡單了,因為函數(shù)不能被比較,所以我們需要添加額外的邏輯去進行移除。
inout 參數(shù)和可變方法
<1> inout 做的事情是通過值傳遞,然后復制回來,而并不是傳遞引用。
<2> lvalue 描述的是一個內(nèi)存地址,它是 “左值 (left value)” 的縮寫,因為 lvalues 是可以存在于賦值語句左側(cè)的表達式。舉例來說,array[0] 是一個 lvalue,它代表的是數(shù)組中第一個元素所在的內(nèi)存值。而 rvalue 描述的是一個值。2 + 2 是一個 rvalue,它描述的是 4 這個值。你不能把 2 + 2 或者 4 放到賦值語句的左側(cè)。
<3> 對于 inout 參數(shù),你只能傳遞 lvalue 給他,因為我們不可能對一個 rvalue 進行改變。
嵌套函數(shù)和 inout
& 不意味 inout 的情況 - UnsafeMutablePointer
計算屬性
有兩種方法和其他普通的方法有所不同,那就是計算屬性和下標方法。計算屬性看起來和常規(guī)的屬性很像,但是它并不使用任何內(nèi)存來存儲自己的值。
相反,這個屬性每次被訪問時,返回值都將被實時計算出來。下標的話,就是一個遵守特殊的定義和調(diào)用規(guī)則的方法。
觀察變更
<1> 屬性觀察者必須在聲明一個屬性的時候就被定義,你無法在擴展里進行追加。所以,這不是一個提供給類型用戶的工具,它是專?為類型的設(shè)計者而設(shè)計的。
<2> willSet 和 didSet 本質(zhì)上是一對屬性的簡寫:一個負責為值提供存儲的私有存儲屬性,以及一個公開的計算屬性。這個計算屬性的 setter 會在將值存儲到存儲屬性中之前和/或之后,進行額外的工作。
<3> 這和 Foundation 中的鍵值觀察有本質(zhì)的不同,鍵值觀察通常是對象的消費者來觀察對象內(nèi)部變化的手段,而與類的設(shè)計者是否希望如此無關(guān)。
<4> 你可以在子類中重寫一個屬性,來添加觀察者。
<5> KVO 使用 Objective-C 的運行時特性, 它動態(tài)地在類的 setter 中添加觀察者,這在現(xiàn)在的 Swift 中,特別是對值類型來說,是無法實現(xiàn)的。Swift 的屬性觀察是一個純粹的編譯時特性。
延遲存儲屬性 - lazy
<1> 訪問一個延遲屬性是 mutating 操作,因為這個屬性的初始值會在第一次訪問時被設(shè)置。
當結(jié)構(gòu)體包含一個延遲屬性時,這個結(jié)構(gòu)體的所有者如果想要訪問該延遲屬性的話,也需要將結(jié)構(gòu)體聲明為可變量,因為訪問這個屬性的同時,也會潛在地對這個屬性的容器進行改變。
<2> 讓想訪問這個延遲屬性的所有 Point 用戶都使用 var 是非常不方便的事情,所以在結(jié)構(gòu)體中使用延遲屬性通常不是一個好主意。
<3> “behaviors”
下標
下標進階
在 Swift 4 中,下標還可以在參數(shù)或者返回類型上使用泛型。
鍵路徑
<1> 鍵路徑是一個指向?qū)傩缘奈凑{(diào)用的引用,它和對某個方法的未使用的引用很類似。- \String.count - .count
<2> 鍵路徑可以由任意的存儲和計算屬性組合而成,其中還可以包括可選鏈操作符。編譯器會自動為所有類型生成 [keyPath:] 的下標方法。你通過這個方法來 “調(diào)用” 某個鍵路徑。對鍵路徑的調(diào)用,也就是在某個實例上訪問由鍵路徑所描述的屬性。所以,"Hello"[keyPath: .count] 等效于 "Hello".count。
可以通過函數(shù)建模的鍵路徑
相對于這樣的函數(shù),鍵路徑除了在語法上更簡潔外,最大的優(yōu)勢在于它們是值。你可以測試鍵路徑是否相等,也可以將它們用作字典的鍵 (因為它們遵守 Hashable)。
另外,不像函數(shù),鍵路徑是不包含狀態(tài)的,所以它也不會捕獲可變的狀態(tài)。
可寫鍵路徑
設(shè)想你要將兩個屬性互相綁定:當屬性 1 發(fā)生變化的時候,屬性 2 的值會自動更新,反之亦然。 可寫的鍵路徑在這種數(shù)據(jù)綁定的過程中會特別有用。
observe(_:options:changeHandler:) + @objc dynamic
鍵路徑層級
→ AnyKeyPath 和 (Any) -> Any? 類型的函數(shù)相似
→ PartialKeyPath<Source> 和 (Source) -> Any? 函數(shù)相似
→ KeyPath<Source, Target> 和 (Source) -> Target 函數(shù)相似
→ WritableKeyPath<Source, Target> 和 (Source) -> Target 與 (inout Source, Target) -> () 這一對函數(shù)相似
→ ReferenceWritableKeyPath<Source, Target> 和 (Source) -> Target 與 (Source, Target) -> () 這一對函數(shù)相似。
第二個函數(shù)可以用 Target 來更新 Source 的值,且要求 Source 是一個引用類型。對 WritableKeyPath 和 ReferenceWritableKeyPath 進行區(qū)分是必要的,前一個類型的 setter 要求它的參數(shù)是 inout 的。
對比 Objective-C 的鍵路徑
自動閉包
短路求值
func and(_ l: Bool, _ r: @autoclosure () -> Bool) -> Bool {
guard l else { return false }
return r()
}
if and(!evens.isEmpty, evens[0] > 10) {
//執(zhí)行操作
}
在 Swift 標準庫中,assert 和 fatalError 也使用了 @autoclosure,因為它們只在確實需要時才對參數(shù)進行求值。
自動閉包在實現(xiàn)日志函數(shù)的時候也很有用。 - 調(diào)試標識符
func log(ifFalse condition: Bool, message: @autoclosure () -> (String), file: String = #file, function: String = #function, line: Int = #line)
{
guard !condition else { return }
print("Assertion failed: \(message()), \(file):\(function) (line \(line))")
}
@escaping 標注
withoutActuallyEscaping
.lazy - 使用延遲的方式進行這些操作的目的是,我們可以在找到第一個不匹配的條目時就立即停止。
它可以讓你把一個非逃逸閉包傳遞給一個期待逃逸閉包作為參數(shù)的函數(shù)。
8. 字符串
Unicode,而非固定寬度
今天的 Unicode 是一個可變?格式。它的可變?特性有兩種不同的意義:由編碼單元 (code unit) 組成 Unicode 標量 (Unicode scalar); 由 Unicode 標量組成字符。
字位簇和標準等價
合并標記
如果你真要進行標準的比較,你必須使用 NSString.compare(_:)。
顏文字
unicodeScalars
比如,NSString 有一個 enumerateSubstrings 方法,能被用來以字位簇的方式枚舉字符串。
字符串和集合
String 是 Character 值的集合。
由字符組成的集合被移動到了 characters 屬性里,它和 unicodeScalars,utf8 以及 utf16 等其他集合視圖類似,是一種字符串的表現(xiàn)形式。
Swift 4 里,String 又成為了 Collection。characters 視圖依然存在,但是僅僅是為了代碼的前向兼容。
雙向索引,而非隨機訪問
String 只實現(xiàn)了 BidirectionalCollection
extension String {
var allPrefixes2: [Substring] {
return [""] + self.indices.map { index in self[...index] }
}
}
hello.allPrefixes2 // ["", "H", "He", "Hel", "Hell", "Hello"]
范圍可替換,而非可變
String 還滿足 RangeReplaceableCollection 協(xié)議。
var greeting = "Hello, world!"
if let comma = greeting.index(of: ",") {
greeting[..<comma] // Hello
greeting.replaceSubrange(comma..., with: " again.")
}
greeting // Hello again.
就算你想要更改的元素只有一個,你也必須使用 replaceSubrange。
字符串索引
下標訪問:因為整數(shù)的下標訪問無法在常數(shù)時間內(nèi)完成 (對于 Collection 協(xié)議來說這也是個直觀要求),而且查找第 n 個 Character 的操作也必須要對它之前的所有字節(jié)進行檢查。
String.Index 是 String 和它的視圖所使用的索引類型,它本質(zhì)上是一個存儲了從字符串開頭的字節(jié)偏移量的不透明值。
index(after:) & index(_:offsetBy:) & limitedBy:
子字符串
它是一個以不同起始和結(jié)束索引的對原字符串的切片。
extension Collection where Element: Equatable {
public func split(separator: Element, maxSplits: Int = Int.max, omittingEmptySubsequences: Bool = true) -> [SubSequence]
}
這個函數(shù)和 String 從 NSString 繼承來的 components(separatedBy:) 很類似,不過還多加了一個決定是否要丟棄空值的選項。
StringProtocol
// 從后向前進行迭代,直到我們找到第一個分隔符,會是更好的策略。
func lastWord(in input: String) -> String? {
// 處理輸?,操作?字符串
let words = input.split(separators: [",", " "])
guard let lastWord = words.last else { return nil }
// 轉(zhuǎn)換為字符串并返回 - 和所有的切片一樣,子字 符串也只能用于短期的存儲,這可以避免在操作過程中發(fā)生昂貴的復制。
return String(lastWord)
}
lastWord(in: "one, two, three, four, five") // Optional("five")
不鼓勵?期存儲子字符串的根本原因在于,子字符串會一直持有整個原始字符串。如果有一個巨大的字符串,它的一個只表示單個字符的子字符串將會在內(nèi)存中持有整個字符串。
?期存儲子字符串實際上會造成內(nèi)存泄漏,由于原字符串還必須被持有在內(nèi)存中,但是它們卻不能再被訪問。
extension Sequence where Element: StringProtocol {
/// 將?個序列中的元素使?給定的分隔符拼接起為新的字符串,并返回
public func joined(separator: String = "") -> String
}
let commaSeparatedNumbers = "1,2,3,4,5"
let numbers = commaSeparatedNumbers.split(separator: ",").flatMap { Int($0) }
// [1, 2, 3, 4, 5]
StringProtocol 設(shè)計之初就是為了在你想要對 String 擴展時來使用的。
編碼單元視圖
String 為此提供了三種視圖: unicodeScalars,utf16 和 utf8,和 String 一樣,它們是雙向索引的集合,并且支持所有我們已經(jīng)熟悉了的操作。
非隨機訪問
共享索引
字符串和它們的視圖共享同樣的索引類型,String.Index。
字符串 和 Foundation
標準庫中的 split 方法和 Foundation 里的 components(separatedBy:)。
另外還有很多其他不匹配的地方: Foundation 使用 ComparisonResult 來表示比較斷言的結(jié)果,而標準庫是圍繞布爾值來設(shè)計斷言的;
像是 trimmingCharacters(in:) 和 components(separatedBy:) 接受一個 CharacterSet 作為參數(shù), 而很不幸,CharacterSet 這個類型的名字在 Swift 中是相當不恰當?shù)摹?enumerateSubstrings(in:options:_:) 這個使用字符串和范圍來對輸入字符串按照字位簇、單詞、句子或者段落進行迭代的超級強力的方法,在 Swift 中對應(yīng)的 API 使用的是子字符串。
其他基于字符串的 Foundation API
用來查詢給定位置的格式屬性的 attributes(at: Int, effectiveRange: NSRangePointer?) 方法,接受的就是一個 (以 UTF-16 測量的) 整數(shù)索引,而非 String.Index,它通過指針返回的 effectiveRange 是一個 NSRange,而非 Range<String.Index>。
你傳遞給 NSMutableAttributedString.addAttribute(_:value:range:) 方法的范圍也遵循同樣的規(guī)則。
字符范圍
… - 不可數(shù)
extension Unicode.Scalar: Strideable {
public typealias Stride = Int
public func distance(to other: Unicode.Scalar) -> Int {
return Int(other.value) - Int(self.value)
}
public func advanced(by n: Int) -> Unicode.Scalar {
return Unicode.Scalar(UInt32(Int(value) + n))!
}
}
let lowercase = ("a" as Unicode.Scalar)..."z"
Array(lowercase.map(Character.init))
/*
["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n",
"o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
*/
CharacterSet
因為它確實就是一個表示一系列 Unicode 標量的數(shù)據(jù)結(jié)構(gòu)體。它完全和 Character 類型不兼容。
String 和 Character 的內(nèi)部結(jié)構(gòu)
從 Objective-C 接收到的字符串背后是一個 NSString。在這種時候,為了讓橋接盡可能高效, NSString 直接扮演了 Swift 字符串的緩沖區(qū)。一個基于 NSString 的 String 在被改變時,將會 被轉(zhuǎn)換為原生的 Swift 字符串。
Character 類型
Character源碼。
簡單的正則表達式匹配器
ExpressibleByStringLiteral
字符串字面量隸屬于 ExpressibleByStringLiteral、 ExpressibleByExtendedGraphemeClusterLiteral 和 ExpressibleByUnicodeScalarLiteral 這三個層次結(jié)構(gòu)的協(xié)議。
extension Regex: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
regexp = value
}
}
let r: Regex = "^h..lo*!$"
CustomStringConvertible 和 CustomDebugStringConvertible
文本輸出流
public func print<Target: TextOutputStream>(_ items: Any..., separator: String = " ", terminator: String = "\n", to output: inout Target)
TextOutputStream
TextOutputStreamable
let toReplace: DictionaryLiteral<String, String>
上面的代碼中,我們使用了 DictionaryLiteral 而不是一個普通的字典。
Dictionary 有兩個副作用: 它會去掉重復的鍵,并且會將所有鍵重新排序。
如果你想要使用像是 [key: value] 這樣的字面量語法,而又不想引入 Dictionary 的這兩個副作用的話,就可以使用 DictionaryLiteral。
DictionaryLiteral 是對于鍵值對數(shù)組 (比如 [(key, value)]) 的很好的替代,它不會引入字典的副作用,同時讓調(diào)用者能夠使用更加便捷的 [:] 語法。
字符串性能
幻影 (phantom) 類型
9. 錯誤處理
除了調(diào)用者必須處理成功和失敗的情況的語法以外,和可選值相比,能拋出異常的方法的主要區(qū)別在于,它可以給出一個包含所發(fā)生的錯誤的詳細信息的值。
Result 類型
拋出和捕獲
注意 Result 是作用于類型上的,而 throws 作用于函數(shù)。
關(guān)鍵字 try 的目的有兩個: 首先,對于編譯器來說這是一個信號,表示我們知道我們將要調(diào)用的函數(shù)可能拋出錯誤。更重要的是,它讓代碼的讀者知道代碼中哪個函數(shù)可能會拋出。
通過調(diào)用一個可拋出的函數(shù),編譯器迫使我們?nèi)タ紤]如何處理可能的錯誤。我們可以選擇使用 do/catch 來處理錯誤,或者把當前函數(shù)也標記為 throws,將錯誤傳遞給調(diào)用棧上層的調(diào)用者。 如果使用 catch 的話,我們可以用模式匹配的方式來捕獲某個特定的錯誤或者所有錯誤。
Swift 的錯誤拋出其實是無類型的
帶有類型的錯誤
func parse(text: String) -> Result<[String], ParseError>
將錯誤橋接到 Objective-C
錯誤和函數(shù)參數(shù)
Rethrows
rethrows 告訴編譯器,這個函數(shù)只會在它的參數(shù)函數(shù)拋出錯誤的時候拋出錯誤。對那些向函數(shù)中傳遞的是不會拋出錯誤的 check 函數(shù)的調(diào)用,編譯器可以免除我們一定要使用 try 來進行調(diào)用的要求。
使用 defer 進行清理
比如你想將代碼的初始化工作和在關(guān)閉時對資源的清理工作放在一起時,就可以使用 defer。 逆序執(zhí)行
錯誤和可選值
我們可以使用 try? 來忽略掉 throws 函數(shù)返回的錯誤,并將返回結(jié)果轉(zhuǎn)換到一個可選值中,來告訴我們函數(shù)調(diào)用是否成功
if let result = try? parse(text: input) {
print(result)
}
錯誤鏈
我們只需要簡單地將這些函數(shù)調(diào)用放到一個 do/catch 代碼塊中 (或者封裝到一個被標記為 throws 的函數(shù)中) 去。
鏈結(jié)果
extension Result {
func flatMap<B>(transform: (A) -> Result<B>) -> Result<B> {
switch self {
case let .failure(m): return .failure(m)
case let .success(x): return transform(x)
}
}
}
高階函數(shù)和錯誤
Result 是異步錯誤處理的正確道路。
不好的地方在于,如果你已經(jīng)在同步函數(shù)中使用 throws 了,再在異步函數(shù)中轉(zhuǎn)為使用 Result 將會在兩種接口之間導入差異。
當我們不能繼續(xù)運行代碼時,可以選擇使用 fatalError 或者是斷言。當我們對錯誤類型不感興趣,或者只有一種錯誤時,我們使用可選值。當我們需要處理多種錯誤,或是想要提供額外的信息時,可以使用 Swift 內(nèi)建的錯誤,或者是自定義一個 Result 類型。當我們想要寫一個接受函數(shù)的函數(shù)時,我們可以使用 rethrows 來讓這個待寫函數(shù)同時接受可拋出和不可拋出的函數(shù)參數(shù)。最后,defer 語句在結(jié)合內(nèi)建的錯誤處理時非常有用。defer 語句為我們提供了集中放置清理代碼的地方,不論是正常退出,還是由于錯誤而被中斷,defer 語句所定義的代碼塊都將被執(zhí)行并進行清理。
10. 泛型
泛型編程的目的是表達算法或者數(shù)據(jù)結(jié)構(gòu)所要求的核心接口。
通過確認核心接口到底是什么,也就是說,找到想要實現(xiàn)的功能的最小需求,我們可以將這個函數(shù)定義在寬闊得多的類型范圍內(nèi)。
重載
擁有同樣名字,但是參數(shù)或返回類型不同的多個方法互相稱為重載方法
自由函數(shù)的重載
func log<View: UIView>(_ view: View) {
print("It's a \(type(of: view)), frame: \(view.frame)")
}
func log(_ view: UILabel) {
let text = view.text ?? "(empty)"
print("It's a label, text: \(text)")
}
運算符的重載
對于重載的運算符,類型檢查器會去使用非泛型版本的重載,而不考慮泛型版本。
使用泛型約束進行重載
實際上 isSubset 并不需要這么具體,在兩個版本中只有兩個函數(shù)調(diào)用,那就是兩者中都有的 contains 以及 Hashable 版本中的 Set.init。
使用閉包對行為進行參數(shù)化
我們可以要求調(diào)用者提供一個函數(shù)來表明元素相等的意義,這樣一來,我們就把判定兩個元素相等的控制權(quán)交給了調(diào)用者。
對集合采用泛型操作
二分查找
泛型二分查找
集合隨機排列
for i in indices.dropLast()
如果 indices 屬性持有了對集合的引用,那么在遍歷 indices 的同時更改集合內(nèi)容,將會讓我們失去寫時復制的優(yōu)化,因為集合需要進行不必要的復制操作。
使用泛型進行代碼設(shè)計
提取共通功能
創(chuàng)建泛型數(shù)據(jù)類型
泛型的工作方式
// → 編譯器不知道(包括參數(shù)和返回值在內(nèi)的)類型為 T 的變量的大小
// → 編譯器不知道需要調(diào)用的 < 函數(shù)是否有重載,因此也不知道需要調(diào)用的函數(shù)的地址。
對于每個泛型類型的參數(shù),編譯器還維護了一系列一個或者多個所謂的目擊表 (witness table): 其中包含一個值目擊表,以及類型上每個協(xié)議約束一個的協(xié)議目擊表。這些目擊表 (也被叫做 vtable) 將被用來將運行時的函數(shù)調(diào)用動態(tài)派發(fā)到正確的實現(xiàn)去。
泛型特化
泛型特化是指,編譯器按照具體的參數(shù)參數(shù)類型 (比如 Int),將 min<T> 這樣的泛型類型或者函數(shù)進行復制。
全模塊優(yōu)化
Swift 中有一個叫做 @_specialize 的非官方標簽,它能讓你將你的泛型代碼進行指定版本的特化,使其在其他模塊中也可用。
另外還有一個相似 (同樣非官方支持) 的 @_inlineable 標簽,當構(gòu)建代碼時,它指導編譯器將被標記函數(shù)的函數(shù)體暴露給優(yōu)化器。這樣,跨模塊的優(yōu)化壁壘就被移除了。
相比 @_specialize, @_inlineable 的優(yōu)勢在于,原來的模塊不需要將具體類型硬編碼成一個列表,因為特化會在使用者的模塊進行編譯時才被施行。
11. 協(xié)議
Swift 的協(xié)議和 Objective-C 的協(xié)議不同。Swift 協(xié)議可以被用作代理,也可以讓你對接口進行抽象 (比如 IteratorProtocol 和 Sequence)。
它們和 Objective-C 協(xié)議的最大不同在于我們可以讓結(jié)構(gòu)體和枚舉類型滿足協(xié)議。除此之外,Swift 協(xié)議還可以有關(guān)聯(lián)類型。我們還可以通過協(xié)議擴展的方式為協(xié)議添加方法實現(xiàn)。
普通的協(xié)議可以被當作類型約束使用,也可以當作獨立的類型使用。
帶有關(guān)聯(lián)類型或者 Self 約束的協(xié)議特殊一些: 我們不能將它當作獨立的類型來使用,所以像是 let x: Equatable 這樣的寫法是不被允許的; 它們只能用作類型約束,比如 func f<T: Equatable>(x: T)。
不過在 Swift 中,Sequence 中的代碼共享是通過協(xié)議和協(xié)議擴展來實現(xiàn)的。通過這么做, Sequence 協(xié)議和它的擴展在結(jié)構(gòu)體和枚舉這樣的值類型中依然可用,而這些值類型是不支持子類繼承的。
協(xié)議擴展是一種可以在不共享基類的前提下共享代碼的方法。
子類必須知道哪些方法是它們能夠重寫而不會破壞父類行為的。
面向協(xié)議編程
協(xié)議的最強大的特性之一就是我們可以以追溯的方式來修改任意類型,讓它們滿足協(xié)議。
協(xié)議擴展
Swift 的協(xié)議的另一個強大特性是我們可以使用完整的方法實現(xiàn)來擴展一個協(xié)議。
通過協(xié)議進行代碼共享相比與通過繼承的共享,有這幾個優(yōu)勢:
→ 我們不需要被強制使用某個父類。
→ 我們可以讓已經(jīng)存在的類型滿足協(xié)議(比如我們讓 CGContext 滿足了 Drawing)。子類就沒那么靈活了,如果 CGContext 是一個類的話,我們無法以追溯的方式去變更它的父類。
→ 協(xié)議既可以用于類,也可以用于結(jié)構(gòu)體,而父類就無法和結(jié)構(gòu)體一起使用了。
→ 最后,當處理協(xié)議時,我們無需擔心方法重寫或者在正確的時間調(diào)用super這樣的問題。
在協(xié)議擴展中重寫方法
協(xié)議要求的方法是動態(tài)派發(fā)的,而僅定義 在擴展中的方法 是靜態(tài)派發(fā)的。
var otherSample: Drawing = SVG()
當我們將 otherSample 定義為 Drawing 類型的變量時,編譯器會自動將 SVG 值封裝到一個代表協(xié)議的類型中,這個封裝被稱作存在容器 (existential container)。
協(xié)議的兩種類型
對于 Sequence 需要實現(xiàn)哪些方法,標準庫在 “滿足 Sequence 協(xié)議” 的部分進行了文檔說明。
類型抹消
<1> 從這次重構(gòu)中,我們可以總結(jié)出一套創(chuàng)建類型抹消的簡單算法。
首先,我們創(chuàng)建一個名為 AnyProtocolName 的結(jié)構(gòu)體或者類。
然后,對于每個關(guān)聯(lián)類型,我們添加一個泛型參數(shù)。
接下來,對于協(xié)議的每個方法,我們將其實現(xiàn)存儲在 AnyProtocolName 中的一個屬性中。
最后,我們添加一個將想要抹消的具體類型泛型化的初始化方法; 它的任務(wù)是在閉包中捕獲我們傳入的對象,并將閉包賦值給上面步驟中的屬性。
<2> 標準庫采用了一種不同的策略來處理類型抹消: 它使用了類繼承的方式,來把具體的迭代器類型隱藏在子類中,同時面向客戶端的類僅僅只是對元素類型的泛型化類型。
帶有 Self 的協(xié)議
協(xié)議內(nèi)幕
func f<C: CustomStringConvertible>(_ x: C) -> Int {
return MemoryLayout.size(ofValue: x)
}
func g(_ x: CustomStringConvertible) -> Int {
return MemoryLayout.size(ofValue: x)
}
f(5) *// 8 *
g(5) *// 40 *
因為 f 接受的是泛型參數(shù),整數(shù) 5 會被直接傳遞給這個函數(shù),而不需要經(jīng)過任何包裝。所以它的大小是 8 字節(jié),也就是 64 位系統(tǒng)中 Int 的尺寸。
對于 g,整數(shù)會被封裝到一個存在容器中。對于普通的協(xié)議 (也就是沒有被約束為只能由 class 實現(xiàn)的協(xié)議),會使用不透明存在容器 (opaque existential container)。
不透明存在容器中含有一個存儲值的緩沖區(qū) (大小為三個指針,也就是 24 字節(jié));一些元數(shù)據(jù) (一個指針,8 字節(jié)); 以及若干個目擊表 (0 個或者多個指針,每個 8 字節(jié))。
如果值無法放在緩沖區(qū)里,那么它將被存儲到堆上,緩沖區(qū)里將變?yōu)榇鎯σ?,它將指向值在堆上的地址。元?shù)據(jù)里包含關(guān)于類型的信息 (比如是否能夠按條件進行類型轉(zhuǎn)換等)。
目擊表是讓動態(tài)派發(fā)成為可能的關(guān)鍵。它為一個特定的類型將協(xié)議的實現(xiàn)進行編碼: 對于協(xié)議中的每個方法,表中會包含一個指向特定類型中的實現(xiàn)的入口。有時候這被稱為 vtable。
typealias Any = protocol<>
MemoryLayout<Any>.size // 32
protocol Prot { }
protocol Prot2 { }
protocol Prot3 { }
protocol Prot4 { }
typealias P = Prot & Prot2 & Prot3 & Prot4
MemoryLayout<P>.size // 64
對于只適用于類的協(xié)議 (也就是帶有 SomeProtocol: class 或者 @objc 聲明的協(xié)議),會有一個叫做 類存在容器 的特殊存在容器,這個容器的尺寸只有兩個字? (以及每個額外的目擊表增加一個字?),一個用來存儲元數(shù)據(jù),另一個 (而不像普通存在容器中的三個) 用來存儲指向這個類的一個引用:
protocol ClassOnly: AnyObject {}
MemoryLayout<ClassOnly>.size // 16
性能影響
但是,如果你想要獲取最大化的性能的時候,使用 泛型參數(shù) 確實要比使用 協(xié)議類型 高效得多。通過使用泛型參數(shù),你可以避免隱式的泛型封裝。
如果你嘗試將一個 [String] (或者其他任何類型) 傳遞給一個接受 [Any] (或者其他任意接受協(xié)議類型,而非具體類型的數(shù)組) 的函數(shù)時,編譯器將會插入代碼對數(shù)組進行映射,將每個值都包裝起來。
這將使方法調(diào)用本身成為一個 O(n) 的操作 (其中 n 是數(shù)組中的元素個數(shù)),這還不包含 函數(shù)體的復雜度。
// 隱式打包
func printProtocol(array: [CustomStringConvertible]) {
print(array)
}
// 沒有打包
func printGeneric<A: CustomStringConvertible>(array: [A]) {
print(array)
}
接口和實現(xiàn)的耦合
使用協(xié)議最大的好處在于它們提供了一種最小的實現(xiàn)接口。
12. 互用性
實踐: 封裝 CommonMark
Swift 的依賴是基于模塊的。
對于 C 或者 Objective-C 的庫來說,想要它們在 Swift 編譯器中可?,庫必須按照 Clang 模塊的格式提供一份模塊地圖 (module map)。模塊地圖中最重要的事情是列舉出組成模塊所使用的頭文件。
封裝 C 代碼庫
封裝 cmark_node 類型
Swift 將 C 枚舉導入為一個包含單個 UInt32 屬性的結(jié)構(gòu)體。除此之外,對原來枚舉中的每個成員,Swift 還會為它生成一個頂層的變量
更安全的接口
低層級類型概覽
// → 含有 managed 的類型代表內(nèi)存是自動管理的。編譯器將負責為你申請,初始化并且釋放內(nèi)存。
// → 含有 unsafe 的類型不提供自動的內(nèi)存管理(這個 managed 正好相反)。你需要明確地進行內(nèi)存申請,初始化,銷毀和回收。
// → 含有 buffer 類型表示作用于一連串的多個元素,而非一個單獨的元素上,它也提供了 Collection 的接口。
// → raw 類型包含無類型的原始數(shù)據(jù),它和 C 的 void* 是等價的。在類型名字中不包含 raw 的類型的數(shù)據(jù)是具有類型的。
// → mutable 類型允許它指向的內(nèi)存發(fā)生改變。
指針
UnsafePointer 是最基礎(chǔ)的指針類型,它與 C 中的 const 指針類似。 UnsafePointer<Int> == const int*。
UnsafeMutablePointer
UnsafeMutableRawPointer 和 UnsafeRawPointer 類型 == void* 或者 const void*
assumingMemoryBound(to:)、bindMemory(to:) 或 load(fromByteOffset:as:)。
在底層,UnsafePointer<T> 和 Optional<UnsafePointer<T>> 的內(nèi)存結(jié)構(gòu)完全相同; 編譯器會將 .none 的 case 映射為一個所有位全為零的 null 指針。
OpaquePointer - cmark_node 的定義并沒有在頭文件中暴露,所以,我們不能訪問到指針指向的內(nèi)存。
Unsafe[Mutable]BufferPointer - 然而,有時候你卻不想為每個元素創(chuàng)建復制。
你通過一個指向起始元素的指針和元素個數(shù)的數(shù)字來初始化這個類型。
Unsafe[Mutable]RawBufferPointer - 類型讓我們可以將那些原始數(shù)據(jù)當作集合來處理,這非常方便 (因為它們可以在底層提供與 Data 和 NSData 等價的類型)。
Tip:
雖然指針需要你手動進行內(nèi)存的分配和釋放,但是它們對于你通過指針存儲的元素, 依然會執(zhí)行標準的 ARC 內(nèi)存管理操作。當你有一個 Pointee 類型是類的非安全可變指針時,它將會對你通過 initialize 存儲在里面的每個對象進行 retain 操作,并且在當你調(diào)用 deinitialize 對它們做 release。
函數(shù)指針
<1> 在 Swift 中,MemoryLayout.size 返回的是一個類型的真實尺寸,但是對于那些在內(nèi)存中的元素,平臺的內(nèi)存對?規(guī)則可能會導致相鄰元素之間存在空隙。 stride 獲取的是這個類型的尺寸,再加上空隙的寬度 (這個寬度可能為 0)。
<2> 當你將代碼從 C 轉(zhuǎn)換為 Swift 時,對于 C 中的 sizeof,在 Swift 中使用 MemoryLayout.stride 會更加合理。
<3> C 函數(shù)指針僅僅只是單純的指針,它們不能捕獲任何值。因為這個原因,編譯器將只允許你提供不捕獲任何局部變量的閉包作為最后一個參數(shù)。@convention(c) 這個參數(shù)屬性就是用來保證這個前提的。
一般化
大多數(shù)和回調(diào)相關(guān)的 C API 都提供另外一種解決方式: 它們接受一個額外的不安全的 void 指針作為參數(shù),并且在調(diào)用回調(diào)函數(shù)時將這個指針再傳遞回給調(diào)用者。
這樣一來,API 的用戶可以在每次調(diào)用這個帶有回調(diào)的函數(shù)時傳遞一小段隨機數(shù)據(jù)進去,然后在回調(diào)中就可以判別調(diào)用者究竟是誰。
public func qsort_r(
_ __base: UnsafeMutableRawPointer!,
_ __nel: Int,
_ __width: Int,
_ __thunk: UnsafeMutableRawPointer!,
_ __compar: @escaping @convention(c)
(UnsafeMutableRawPointer?, UnsafeRawPointer?, UnsafeRawPointer?)
-> Int32
)
typealias Block = (UnsafeRawPointer?, UnsafeRawPointer?) -> Int32
func qsort_block(_ array: UnsafeMutableRawPointer, _ count: Int, _ width: Int, f: @escaping Block)
{
var thunk = f
qsort_r(array, count, width, &thunk) { (ctx, p1, p2) -> Int32 in
let comp = ctx!.assumingMemoryBound(to: Block.self).pointee
return comp(p1, p2)
}
}
extension Array where Element: Comparable {
mutating func quicksort() {
qsort_block(&self, self.count, MemoryLayout<Element>.stride) { a, b in
let l = a!.assumingMemoryBound(to: Element.self).pointee
let r = b!.assumingMemoryBound(to: Element.self).pointee
if r > l { return -1 }
else if r == l { return 0 }
else { return 1 }
}
}
}
var x = [3,1,2]
x.quicksort()
x // [1, 2, 3]
不過,除了排序以外,還有很多有意思的 C API。而將它們以 類型安全 和 泛型接口 的方式進行封裝所用到的技巧,與我們上面的例子是一致的。