一、概述
Swift 4.1 是 Swift 4 的第一個小版本更新,主要包括一些很實用的改進,例如,自動合成 Equatable 和 Hashable,協(xié)議條件約束,檢測模擬器環(huán)境等等。例子工程地址
二、自動合成 Equatable 和 Hashable
Equatable
協(xié)議允許 Swfit 中相同類型的兩個實例之前的比較。當我們寫 5 == 5
的時候,Swift 之所以能夠理解是因為 Int
遵守 Equtable
協(xié)議,意味著它實現(xiàn)了一個 ==
函數(shù)描述兩個 Int 之間的關(guān)系。
然而,實現(xiàn) Equatable
有點蛋疼,看下面的代碼:
struct Person {
var name: String
}
如果你有兩個 Person 實例,并且想要確保他們的一致性,需要比較他們的所有屬性,具體如下:
// Swift 4.0 的實現(xiàn)
struct Person: Equatable {
var name: String
static func ==(lhs: Person, rhs: Person) -> Bool {
return lhs.name == rhs.name
}
}
// Swift 4.1 實現(xiàn)
struct Person: Equatable {
var name: String
}
上面的代碼讀起來很枯燥,寫起來更蛋疼。比較幸運的是,Swift 4.1 能夠自動合成 Equatable
協(xié)議中約定的方法,也就是自動生成 ==
方法,方法中會比較兩個對象間的所有屬性是否相等。現(xiàn)在你只需要在指定的類型上添加 遵守Equatable
協(xié)議,其他的操作 Swift 會自動完成。
當然,如果你想你也可以自己實現(xiàn) ==
。例如,如果你的類型有一個標記是否唯一的 id
,你需要自己寫 ==
來只比較這個值,而不是讓 Swift
來完成所有其他的工作。
Swift 4.1 也給 Hashable
協(xié)議提供了自動合成的支持,意味著會自動合成 hashValue
屬性。Hashable
通常情況下實現(xiàn)比較蛋疼,因為需要返回一個唯一的(或者大多數(shù)情況下唯一的)hash
值。這一點很重要,因為它可以使對象作為字典的 keys
并且存儲在 Set
中。
// Swift 4.0 實現(xiàn)
struct Person: Hashable {
var name: String
var hashValue: Int {
return name.hashValue
}
static func ==(lhs: HashPerson, rhs: HashPerson) -> Bool {
return lhs.name == rhs.name
}
}
// Swift 4.1 實現(xiàn)
struct Person: Hashable {
var name: String
}
雖然大多數(shù)情況下不用自己實現(xiàn),但是如果你想做些特別的事情的時候也可以自己實現(xiàn)。
注:現(xiàn)在我們?nèi)匀恍枰岊愋妥袷貐f(xié)議,自動合成需要類型的所有屬性都分別遵守了 Equatable
或者 Hashable
協(xié)議。
更多信息,請參照 Swift Evolution proposal SE-0185
三、Codable Key 編解碼策略優(yōu)化
- 之前寫過一篇完整的文章來描述這個特性:Swift 4.1 improves Codable with keyDecodingStrategy
在 Swift 4.0 中使用 Codable
協(xié)議一個常見問題就是,JSON
中使用蛇形命名法作為 key
的名字,而 Swift
中使用駝峰命名法。Codable
不能夠理解兩種命名的差別,必須創(chuàng)建自定義的 CodingKeys
枚舉來解決這個問題。
基于上面的原因,Swift 4.1 中引入了 keyDecodingStrategy
屬性。默認為 .useDefaultKeys
,直接映射 JSON
名字到Swift
屬性。可以使用 .convertFromSnakeCase
來讓 Codable
處理名字轉(zhuǎn)換。
let decoder = JSONDecoder()
do {
decoder.keyDecodingStrategy = .convertFromSnakeCase
let macs = try decoder.decode([Mac].self, from: jsonData)
print(macs)
} catch {
print(error.localizedDescription)
}
反之,如果你想將遵守 Codable
協(xié)議的 struct
類型轉(zhuǎn)成 JSON
,Struct
的屬性是駝峰命名的,轉(zhuǎn)成的JSON
是蛇形命名的,設(shè)置 keyEncodingStrategy
為 .convertToSnakeCase
。
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let encoded = try encoder.encode(someObject)
四、協(xié)議的條件遵守
Swift 4.1 實現(xiàn)了 SE-0143 提案,引入了條件遵守協(xié)議。具體為只有當滿足指定條件的時候,類型才能遵守協(xié)議。
舉例來講,我們現(xiàn)在聲明一個可以用來買東西的協(xié)議 Purchaseable
。
protocol Purchaseable {
func buy()
}
現(xiàn)在可以定義一個 Book
結(jié)構(gòu)體,遵守 Purchaseable
協(xié)議,在買一本書的時候打印消息。
struct Book: Purchaseable {
func buy() {
print("You bought a book")
}
}
這個場景很簡單,讓我們更進一步。如果這個用戶有一個裝滿書籍的籃子,并且想把籃子里所有的書全都買下來。我們當然可以遍歷數(shù)組,然后調(diào)用每本書的 buy
方法。但是更好的方式是給 Array
寫個 Extension
遵守 Purchaseable
協(xié)議,然后實現(xiàn)協(xié)議 buy
方法,調(diào)用每個 Element
的 buy
方法。
基于上面的原因,Swift 4.1 引入了 Conditional Conformances
。如果我們嘗試拓展數(shù)組,會有一定的副作用。例如會給一個字符串數(shù)組添加 buy
方法,而字符串沒有 buy
方法供我們調(diào)用。
Swift 4.1 可以實現(xiàn)只有當數(shù)組中的元素是遵守 Purchaseable
,數(shù)組才能遵守 Purchaseable
協(xié)議。
extension Array: Purchaseable where Element: Purchaseable {
func buy() {
for item in self {
item.buy()
}
}
}
如你所見,協(xié)議的條件遵守,讓我們以更簡潔的方式給拓展添加協(xié)議支持。
同樣的,協(xié)議的條件遵守也使我們的 swift 代碼更加簡單和安全,并且我們也不需要做些額外的工作。例如,創(chuàng)建兩個可選字符串的數(shù)組并且比較它們是否相等。
ar left: [String?] = ["Andrew", "Lizzie", "Sophie"]
var right: [String?] = ["Charlotte", "Paul", "John"]
left == right
上面的例子看起來不那么重要,但是 Swift 4.0 上面的語法不能編譯,String
和 [String]
是 Equatable
,但是 [String?]
不是。
Swift 4.1 中的協(xié)議的條件約束指的是只要滿足指定的條件,就可以遵守協(xié)議。上面的例子中,如果數(shù)組中的元素是遵守 Equatble
的,那么數(shù)組就是遵守 Equatable
協(xié)議。所以,上面的代碼在 Swift 4.1 上可以編譯通過。
協(xié)議的條件遵守也適用于 Codable
協(xié)議,并且使代碼變得更加安全。
import Foundation
struct Person {
var name = "Taylor"
}
var people = [Person()]
var encoder = JSONEncoder()
// try encoder.encode(people)
如果將 encoder.encode(people)
的注釋打開,在 Swift 4.1 中編譯不通過,因為試圖 encode
一個不遵守 Codable
協(xié)議的類型。然而,這段代碼在 swift 4.0 上面是可以編譯通過的,但是因為 Person
不遵守 Codable
協(xié)議會導致在運行時崩潰。
很明顯,大家都不想要運行時崩潰。幸運的是,Swift 4.1 使用協(xié)議的條件遵守幫我們清除了這個障礙,Optional
、Array
、Dictionary
和 Set
只有在他們的內(nèi)容遵守 Codable
協(xié)議的時候,自身才遵守協(xié)議,所以上面的代碼在 Swift 4.1 會編譯不過。
五、關(guān)聯(lián)類型的遞歸約束
Swift 4.1 實現(xiàn)了 SE-0157 提案,增強了協(xié)議內(nèi)部使用關(guān)聯(lián)類型的限制。現(xiàn)在可以給關(guān)聯(lián)類型創(chuàng)建一個遞歸的約束,就是關(guān)聯(lián)類型可以用自身所在協(xié)議來約束自己。
我們以技術(shù)公司的管理層級來闡述這個問題,在一個公司,每一個雇員都有一個上司,每個上司必須有一個以上的下屬。我們以一個 Employee
協(xié)議來表明這樣的關(guān)系:
protocol Employee {
associatedtype Manager: Employee
var manager: Manager? { get set }
}
盡管這是一個不言而喻的關(guān)系,但是 Swift 4.0 上這段代碼卻編譯不過,因為在協(xié)議內(nèi)部使用了自己。
感謝這個新特性,我們可以模擬一個包含三種團隊角色的技術(shù)公司,初級開發(fā)工程師、高級開發(fā)工程師和董事會成員。
class BoardMember: Employee {
var manager: BoardMember?
}
class SeniorDeveloper: Employee {
var manager: BoardMember?
}
class JuniorDeveloper: Employee {
var manager: SeniorDeveloper?
}
注:這邊用 Class
而不是 Struct
,是因為 BoardMember
里面包含一個 BoardMember
,如果用結(jié)構(gòu)體會形成無窮大的結(jié)構(gòu)體。如果這里面有一個 Class
,我個人傾向于使用全部采用 Class
來保持一致。如果你想要使用結(jié)構(gòu)體,可以把 JuniorDeveloper
和 SeniorDeveloper
設(shè)置成結(jié)構(gòu)體。
六、模塊引入檢測
Swift 4.1 實現(xiàn)了 SE-0075 提案,引入了一個新的 canImport
條件來幫助我們在編譯期檢測一個指定的模塊能否被導入。
這個特性對跨平臺的代碼很有用,例如你的代碼在 macOS
和 iOS
行為不一樣,或者你需要 Linux
的功能。
#if canImport(SpriteKit)
// this will be true for iOS, macOS, tvOS, and watchOS
#else
// this will be true for other platforms, such as Linux
#endif
之前我們必須通過判斷平臺信息來處理這種情況。
#if !os(Linux)
// Matches macOS, iOS, watchOS, tvOS, and any other future platforms
#endif
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
// Matches only Apple platforms, but needs to be kept up to date as new platforms are added
#endif
新特性 canImport
讓我們更好的關(guān)注功能,而不是當前編譯的平臺,避免了很多蛋疼的問題。
七、模擬器環(huán)境檢測
Swift 4.1 實現(xiàn)了 SE-0190 提案,引入了 targetEnvironment
條件,幫助我們更好的區(qū)分模擬器和真機。現(xiàn)在 targetEnvironment
只有一個值 simulator
,當是模擬器設(shè)備的時候,返回 true
。
#if targetEnvironment(simulator)
// code for the simulator here
#else
// code for real devices here
#endif
當代碼用來處理類似于從攝像頭讀取數(shù)據(jù)或者訪問陀螺儀數(shù)據(jù)等模擬器不支持的功能的時候,這個條件判斷很有用。舉個例子,從攝像頭選擇照片,如果是真機,創(chuàng)建和配置 UIImagePickerController()
方法 ,如果是模擬器,從 Bundle
中讀取一張圖片。
import UIKit
class TestViewController: UIViewController, UIImagePickerControllerDelegate {
// a method that does some sort of image processing
func processPhoto(_ img: UIImage) {
// process photo here
}
// a method that loads a photo either using the camera or using a sample
func takePhoto() {
#if targetEnvironment(simulator)
// we're building for the simulator; use the sample photo
if let img = UIImage(named: "sample") {
processPhoto(img)
} else {
fatalError("Sample image failed to load")
}
#else
// we're building for a real device; take an actual photo
let picker = UIImagePickerController()
picker.sourceType = .camera
vc.allowsEditing = true
picker.delegate = self
present(picker, animated: true)
#endif
}
// this is called if the photo was taken successfully
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
// hide the camera
picker.dismiss(animated: true)
// attempt to retrieve the photo they took
guard let image = info[UIImagePickerControllerEditedImage] as? UIImage else {
// that failed; bail out
return
}
// we have an image, so we can process it
processPhoto(image)
}
}
八、FlatMap 部分場景更名 CompactMap
FlatMap
在 Swift 4.0 中很有用,特別是在轉(zhuǎn)換集合中的對象,并且移除其中的 nil
對象的時候。Swift 提案 SE-0187 中有對這部分內(nèi)容更改的說明,在 Swift 4.1 中 flatMap
為了語義更加清晰,已經(jīng)更名成 compactMap
。
let array = ["1", "2", "Fish"]
let numbers = array.compactMap { Int($0) }
上面例子的結(jié)果是 [1, 2]
。
九、展望 Swift 5
引入?yún)f(xié)議的條件遵守已經(jīng)使 Swift 團隊提升穩(wěn)定性的同時,移除了大量代碼,自動合成 Equatable
和 Hashable
的支持也使我們開發(fā)更加便捷。其他一些在開發(fā)或者在 Review
的提案,包括 SE-0192: Non-Exhaustive Enums, SE-0194: Derived Collection of Enum Cases,和 SE-0195: Dynamic Member Lookup – click here to learn more about new Swift features coming in 2018。同這些新特性一樣重要的是,蘋果計劃在今年實現(xiàn) Swift 的 ABI
穩(wěn)定,期待??????。