? ? 本文翻譯自raywenderlich.com中的文章《What’s New in Swift 4?》,由于本人水平有限,翻譯中不準確或者有錯誤的地方,敬請諒解和指正。
? ? 提示:本教程使用集成在 Xcode 9 beta 1 中Swift 4 版本。
? ? Swift 4 是蘋果公司計劃于2017年秋季發布的最新主要版本,它主要關注與Swift 3 的兼容性,以及繼續向ABI穩定邁進。
? ? 皮皮Swift 4,我們走
? ? Swift 4 包含于Xcode 9中。你可以從蘋果公司的開發者頁面下載最新的Xcode 9(你必須有一個開發者賬號)。每個Xcode版本將會包含Swift 4當時版本的快照。
? ? 當你閱讀時,你會發現鏈接都是SE-xxxx形式。這些鏈接將會為你展示相關的Swift演化提案。如果你想深入學習某個專題,可以翻閱它們。
? ? 我推薦在playground中嘗試每個 Swift 4 的特性或更新。這將幫助強化你頭腦中的知識,賦予你深入學習每個專題的能力。擴展他們、打破它們,與playground中的例子斗,其樂無窮。
? ? 提示:這篇文章將隨著每個Xcode beta版本更新。如果你使用不同的快照,不能保證代碼可以運行。
? ? 移植到 Swift 4
? ? Swift 3 移植到 4 要比 2.2 到 3容易一些。總的來說,大多數變化都是新增的,不需要大量的個人情懷(personal touch)。因此,Swift移植工具將為你完成大部分的修改工作量。
? ? Xcode 9同時支持 Swift 4 以及Swift 3 的中間版本 Swift 3.2。項目中的target既可以是 Swift 3.2 也可以是 Swift 4,讓你在需要的時候一點一點移植。轉換到 Swift 3.2不是完全沒有工作量,你需要更新部分代碼來兼容新的SDK,再者因為 Swift 還沒有ABI穩定,所以你需要用Xcode 9 重新編譯依賴關系。
? ? 當你準備移植到 Swift 4,Xcode再次提供了移植工具幫助你。Xcode中,你可以使用“Edit/Convert/To Current Swift Syntax”啟動轉換工具。
? ? 選擇了想要轉換的target之后,Xcode將提示你Objective-C推斷的偏好設置,選擇推薦選項以通過限制推斷來減少二進制大小(更多這個專題,查看下面的限制@objc推斷)
? ? 為了更好的理解你代碼中將會出現的變化,我們先來看看 Swift 4中的API 變化。
? ? API變化
在轉入Swift 4 新增特性之前,讓我們先看下已有API的變化或更新。
? ? 字符串Strings
? ? Swift 4 中String類收到頗多關愛,這份提案包含很多變化,來看看最大變化【SE-0163】。
假如你感覺懷舊,strings像Swift 2.0之前那樣再次成為集合。這一變化移除了String必須的character數組。現在你可以直接在String對象上迭代。
let galaxy = "Milky Way ??"
for char in galaxy {
print(char)
}
? ? 你不僅可以對String做邏輯迭代,還可以得到Sequence和Collection中的額外特性。
galaxy.count? ? ? // 11
galaxy.isEmpty? ? // false
galaxy.dropFirst() // "ilky Way ??"
String(galaxy.reversed()) // "?? yaW ykliM"
// Filter out any none ASCII characters
galaxy.filter { char in
let isASCII = char.unicodeScalars.reduce(true, { $0 && $1.isASCII })
return isASCII
} // "Milky Way "
? ? 上面的ASCII例子闡述了對Character的小改進。你可以直接訪問Character的UnicodeScalarView屬性。之前你需要生成一個新String【SE-0178】。
? ? 另外一個新增特性是StringProtocol。它聲明了大部分String中聲明的功能。這樣做是為改進slice如何工作。Swift 4 增加了 Substring類型來引用String的子序列。
? ? String和Substring都實現StringProtocol協議,所以他們功能的幾乎完全相同:
// Grab a subsequence of String
let endIndex = galaxy.index(galaxy.startIndex, offsetBy: 3)
var milkSubstring = galaxy[galaxy.startIndex...endIndex]? // "Milk"
type(of: milkSubstring)? // Substring.Type
// Concatenate a String onto a Substring
milkSubstring += "??"? ? // "Milk??"
// Create a String from a Substring
let milkString = String(milkSubstring) // "Milk??"
另外一個值得點贊的改進是,String如何表示字母簇。這一決議來源于Unicode 9 的適配。原先,多編碼點的unicode字符長度大于1,常見于emoji表情。下面是一些常見的例子:
"?????".count // Now: 1, Before: 2
"????".count // Now: 1, Before: 2
"???????????".count // Now: 1, Before, 4
? ? 這只是String Manifesto中的一部分變化,你可以在String Manifesto讀到所有這些的原始動機,以及未來可預見的解決提案。
? ? 字典和集合
? ? 集合類型不斷發展,Set和Dictionary不總是最直觀的。幸運的是,Swift 團隊給了他們很多的關注【SE-0165】
? ? 基于序列的初始化
? ? 第一項能力就是用鍵值對序列創建字典:
let nearestStarNames = ["Proxima Centauri", "Alpha Centauri A", "Alpha Centauri B", "Barnard's Star", "Wolf 359"]
let nearestStarDistances = [4.24, 4.37, 4.37, 5.96, 7.78]
// Dictionary from sequence of keys-values
let starDistanceDict = Dictionary(uniqueKeysWithValues: zip(nearestStarNames, nearestStarDistances))
// ["Wolf 359": 7.78, "Alpha Centauri B": 4.37, "Proxima Centauri": 4.24, "Alpha Centauri A": 4.37, "Barnard's Star": 5.96]
? ? 重復Key解決方案
? ? 你現在可以在初始化字典時,使用任意方法處理重復key問題。這可以避免“無提示的改寫鍵值對”。
// Random vote of people's favorite stars
let favoriteStarVotes = ["Alpha Centauri A", "Wolf 359", "Alpha Centauri A", "Barnard's Star"]
// Merging keys with closure for conflicts
let mergedKeysAndValues = Dictionary(zip(favoriteStarVotes, repeatElement(1, count: favoriteStarVotes.count)), uniquingKeysWith: +) // ["Barnard's Star": 1, "Alpha Centauri A": 2, "Wolf 359": 1]
? ? ?上面的代碼使用zip和“+”號,將重復key對應的值相加。
? ? 提示:如果你不熟悉zip,你可以快速的在蘋果的Swift文檔中學習。
? ? 篩選
? ? 字典和集合(Set)現在都獲取了篩選能力,將篩選結果放入原始類型的新對象中。
// Filtering results into dictionary rather than array of tuples
let closeStars = starDistanceDict.filter { $0.value < 5.0 }
closeStars // Dictionary: ["Proxima Centauri": 4.24, "Alpha Centauri A": 4.37, "Alpha Centauri B": 4.37]
? ? 字典映射
字典獲得了一個非常有用的方法,可以直接映射它的值:
// Mapping values directly resulting in a dictionary
let mappedCloseStars = closeStars.mapValues { "\($0)" }
mappedCloseStars // ["Proxima Centauri": "4.24", "Alpha Centauri A": "4.37", "Alpha Centauri B": "4.37"]
? ? ?字典缺省值
? ? 訪問字典中的值時,常見的實踐方法,是使用??操作符,當值為nil時賦予缺省值。在 Swift 4 中,這項操作變得簡潔,允許你在代碼行上“耍花槍”。
// Subscript with a default value
let siriusDistance = mappedCloseStars["Wolf 359", default: "unknown"] // "unknown"
// Subscript with a default value used for mutating
var starWordsCount: [String: Int] = [:]
for starName in nearestStarNames {
let numWords = starName.split(separator: " ").count
starWordsCount[starName, default: 0] += numWords // Amazing
}
starWordsCount // ["Wolf 359": 2, "Alpha Centauri B": 3, "Proxima Centauri": 2, "Alpha Centauri A": 3, "Barnard's Star": 2]
? ? 以前這種變換都必須包裹在臃腫的 if-let 語句中,在 Swift 4 中,只需簡單一行代碼。
? ? 字典組
? ? 另外一個很有用的新增特性是,用序列初始化字典并把他們組合在一起:
// Grouping sequences by computed key
let starsByFirstLetter = Dictionary(grouping: nearestStarNames) { $0.first! }
// ["B": ["Barnard's Star"], "A": ["Alpha Centauri A", "Alpha Centauri B"], "W": ["Wolf 359"], "P": ["Proxima Centauri"]]
這在處理特殊形式的數據時變得很便利。
? ? 預留容量
? ? 序列和字典都有了顯式預留容量的能力。
// Improved Set/Dictionary capacity reservation
starWordsCount.capacity? // 6
starWordsCount.reserveCapacity(20) // reserves at _least_ 20 elements of capacity
starWordsCount.capacity // 24
? ? 對序列和字典重新分配內存空間,是非常耗費資源的操作。當你知道需要多少數據時,使用reserveCapacity(_:)是提升性能的簡單方法。
? ? 這包含了很多信息,所以查看這兩個類型的文檔,找到優化代碼的方法。
? ? 私有訪問修飾符
? ? 有些人不喜歡 Swift 3 中新增的 fileprivate。理論上,它很贊,但實際上它的用法讓人糊涂。這樣做的目的是,在成員內部使用private,而在相同文件中想要在成員中分享訪問權限則使用 fileprivate。
? ? 這個問題源自于,Swift鼓勵使用擴展讓代碼邏輯分組。擴展被看做是原始成員聲明作用域的外圍,使得擴展需要 fileprivate。
? ? Swift 4 認識到了在類型及其擴展之間分享相同訪問作用域的原始動機,只有在相同的源文件中才起作用【SE-0169】:
struct SpaceCraft {
private let warpCode: String
init(warpCode: String) {
self.warpCode = warpCode
}
}
extension SpaceCraft {
func goToWarpSpeed(warpCode: String) {
if warpCode == self.warpCode { // Error in Swift 3 unless warpCode is fileprivate
print("Do it Scotty!")
}
}
}
let enterprise = SpaceCraft(warpCode: "KirkIsCool")
//enterprise.warpCode? // error: 'warpCode' is inaccessible due to 'private' protection level
enterprise.goToWarpSpeed(warpCode: "KirkIsCool") // "Do it Scotty!"
? ? 這樣,fileprivate就回歸了本來的目的,而不是作為代碼組織結構的繃帶。
? ? 新增API
? ? 現在讓我們看看 Swift 4 的亮瞎雙眼的新特性。這些特性不會打破已有代碼,因為他們都是簡單新增。
? ? 歸檔和序列化
? ? 講到這里,序列化和歸檔自定義類型,你需要一些套路。對于類,你需要創建NSObject的子類、實現NSCoding協議。
? ? struct和enum等值類型則需要一些小技巧,像創建能夠繼承NSObject和NSCoding的子類。
? ? Swift 4 引入這三種類型的序列化來解決這個問題【SE-0166】。
struct CuriosityLog: Codable {
enum Discovery: String, Codable {
case rock, water, martian
}
var sol: Int
var discoveries: [Discovery]
}
// Create a log entry for Mars sol 42
let logSol42 = CuriosityLog(sol: 42, discoveries: [.rock, .rock, .rock, .rock])
? ? ?從例子可以看出,只要實現 Codable 協議就可以使得Swift類型可編碼和解碼。如果所有屬性都是Codable,編譯器自動生成協議的實現。
? ? 真正對一個對象編碼,你需要將它傳給一個編碼器。Swift 4 正在積極推進Swift編碼器。根據不同的模式給對象編碼。【SE-0167】(注意:這個提案的部分內容還在修訂中)。
let jsonEncoder = JSONEncoder() // One currently available encoder
// Encode the data
let jsonData = try jsonEncoder.encode(logSol42)
// Create a String from the data
let jsonString = String(data: jsonData, encoding: .utf8) // "{"sol":42,"discoveries":["rock","rock","rock","rock"]}"
? ? 上面代碼將一個對象自動編碼為JSON對象。注意檢查JSONEncoder暴露出來的屬性以便自定義輸出結果。
? ? 最后是將數據解碼到一個具體對象:
let jsonDecoder = JSONDecoder() // Pair decoder to JSONEncoder
// Attempt to decode the data to a CuriosityLog object
let decodedLog = try jsonDecoder.decode(CuriosityLog.self, from: jsonData)
decodedLog.sol? ? ? ? // 42
decodedLog.discoveries // [rock, rock, rock, rock]
? ? 有了 Swift 4,編碼和解碼變得類型安全,不需要依賴@objc協議的限制。
? ? 鍵值編碼
? ? 因為函數在Swift中是閉包,所以你可以持有函數的引用而不調用它們。但做不到的是,持有屬性的引用而不實際訪問屬性下的數據。
? ? Swift 4 新增了讓人興奮的特性,可以引用類型的keypath來get/set實例的底層數據。
struct Lightsaber {
enum Color {
case blue, green, red
}
let color: Color
}
class ForceUser {
var name: String
var lightsaber: Lightsaber
var master: ForceUser?
init(name: String, lightsaber: Lightsaber, master: ForceUser? = nil) {
self.name = name
self.lightsaber = lightsaber
self.master = master
}
}
let sidious = ForceUser(name: "Darth Sidious", lightsaber: Lightsaber(color: .red))
let obiwan = ForceUser(name: "Obi-Wan Kenobi", lightsaber: Lightsaber(color: .blue))
let anakin = ForceUser(name: "Anakin Skywalker", lightsaber: Lightsaber(color: .blue), master: obiwan)
? ? 這里創建了一些force的實例,設置了名字、光劍和主人。要創建key path,你只需要在屬性名前加上右斜線:
// Create reference to the ForceUser.name key path
let nameKeyPath = \ForceUser.name
// Access the value from key path on instance
let obiwanName = obiwan[keyPath: nameKeyPath]? // "Obi-Wan Kenobi"
? ? ?上面例子中,為ForceUser的name屬性創建了一個key path,你可以把它傳遞給keyPath參數。下面是一些例子,使用key path擴展到子對象、設置屬性等。
// Use keypath directly inline and to drill down to sub objects
let anakinSaberColor = anakin[keyPath: \ForceUser.lightsaber.color]? // blue
// Access a property on the object returned by key path
let masterKeyPath = \ForceUser.master
let anakinMasterName = anakin[keyPath: masterKeyPath]?.name? // "Obi-Wan Kenobi"
// Change Anakin to the dark side using key path as a setter
anakin[keyPath: masterKeyPath] = sidious
anakin.master?.name // Darth Sidious
// Note: not currently working, but works in some situations
// Append a key path to an existing path
//let masterNameKeyPath = masterKeyPath.appending(path: \ForceUser.name)
//anakin[keyPath: masterKeyPath] // "Darth Sidious"
? ? Swift中key path的優雅之處在于它們是強類型的,不再有Objective-C的雜亂。
? ? 多行String字面量
? ? 很多編程語言的特性中包含創建多行字符串字面量的能力。Swift 4用三個引號"""包裹文本,實現了這個簡單但有用的語法。
let star = "??"
let introString = """
A long time ago in a galaxy far,
far away....
You could write multi-lined strings
without "escaping" single quotes.
The indentation of the closing quotes
below deside where the text line
begins.
You can even dynamically add values
from properties: \(star)
"""
print(introString) // prints the string exactly as written above with the value of star
? ? 這在創建XML/JSON信息或者創建長格式文本時,非常有用。
? ? 單邊范圍
? ? 為減少冗長、提高可讀性,標準庫可以使用單邊范圍確定開始、結束索引。這使得從集合截取變得方便。
// Collection Subscript
var planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
let outsideAsteroidBelt = planets[4...] // Before: planets[4..
? ?能看到,單邊范圍減少了顯示指定開始、結束索引的必要。
? ? 無窮序列
? ? 當開始索引是可數類型時,你可以創建一個無窮序列:
// Infinite range: 1...infinity
var numberedPlanets = Array(zip(1..., planets))
print(numberedPlanets) // [(1, "Mercury"), (2, "Venus"), ..., (8, "Neptune")]
planets.append("Pluto")
numberedPlanets = Array(zip(1..., planets))
print(numberedPlanets) // [(1, "Mercury"), (2, "Venus"), ..., (9, "Pluto")]
? ? 模式匹配
? ? 單邊范圍的另一個用途是模式匹配:
// Pattern matching
func temperature(planetNumber: Int) {
switch planetNumber {
case ...2: // anything less than or equal to 2
print("Too hot")
case 4...: // anything greater than or equal to 4
print("Too cold")
default:
print("Justtttt right")
}
}
temperature(planetNumber: 3) // Earth
? ? 泛型下標
? ? 下標是訪問數據類型的重要組成部分,同時也非常直觀。為提升這種有用性,下標現在可支持泛型【SE-0148】:
struct GenericDictionary<Key : Hashable, Value>{??
? ? private var data: [Key: Value]??
? ? init(data: [Key: Value]) {? ??
? ? ? ? self.data = data??
? ? }??
? ? subscript<T>(key: Key) -> T? {
? ? ? ? return data[key] as? T
? ? }
}
? ? 例子中返回類型為泛型,你可以這樣使用泛型下標:
// Dictionary of type: [String: Any]
var earthData = GenericDictionary(data: ["name": "Earth", "population": 7500000000, "moons": 1])
// Automatically infers return type without "as? String"
let name: String? = earthData["name"]
// Automatically infers return type without "as? Int"
let population: Int? = earthData["population"]
? ?不僅返回類型可以為泛型,下標類型也可以為泛型:
extension GenericDictionary {? subscript(keys: Keys) -> [Value] where Keys.Iterator.Element == Key {
var values: [Value] = []
for key in keys {
if let value = data[key] {
values.append(value)
}
}
return values
}
}
// Array subscript value
let nameAndMoons = earthData[["moons", "name"]]? ? ? ? // [1, "Earth"]
// Set subscript value
let nameAndMoons2 = earthData[Set(["moons", "name"])]? // [1, "Earth"]
? ? 本例中,你可以傳入兩個不同的序列類型(數組和集合),得到相關值的數組。
? ? 雜項
? ? MutableCollection現在有了可變方法swapAt(_:_:),正向看起來的那樣,它交換給定數組中的值。
// Very basic bubble sort with an in-place swap
func bubbleSort(_ array: [T]) -> [T] {??
? ? var sortedArray = array??
? ? for i in 0..sortedArray[j] {
? ? ? ? sortedArray.swapAt(j-1, j) // New MutableCollection method
? ? ?}
? ?}
}
return sortedArray
}
bubbleSort([4, 3, 2, 1, 0]) // [0, 1, 2, 3, 4]
? ? ? 關聯類型約束
可以通過where語句包含關聯類型約束【SE=0142】:
protocol MyProtocol {
? ? associatedtype Element
? ? associatedtype SubSequence : Sequence where SubSequence.Iterator.Element == Iterator.Element
}
? ? 使用協議約束,很多associatedtype聲明可以直接包含自身的值,而不需要其他套路。
? ? 類和協議的存在性
? ? 區分Objective-C和Swift的重要特性就是,Swift可以定義一個類型,既遵從一個類又遵從一些協議【SE-0156】:
protocol MyProtocol { }
class View { }
class ViewSubclass: View, MyProtocol { }
class MyClass {
var delegate: (View & MyProtocol)?
}
let myClass = MyClass()
//myClass.delegate = View() // error: cannot assign value of type 'View' to type '(View & MyProtocol)?'
myClass.delegate = ViewSubclass()
? ? 限制@objc推斷
? ? 要提供Swift API給Objective-C,你要使用@objc編譯器屬性。很多情況下,Swift編譯器可以為你推斷。推斷的三個主要問題在于:
? ? 1、二進制大小大幅度增加的隱患
? ? 2、不確定@objc何時被推斷。
? ? 3、無意間造成和Objective-C函數沖突的可能性增加。
Swift 4拿出大板斧,限制了@objc推斷【SE-0160】。這意味著,當你需要Objective-C全部動態分發能力時,必須顯式使用@objc。
? ? NSNumber橋接
? ?NSNumber和Swift numbers之間有很多惱人的惡臭,縈繞在語言周圍太長時間了。幸運的是,Swift解決了它們【SE-0170】。
let n = NSNumber(value: 999)
let v = n as? UInt8 // Swift 4: nil, Swift 3: 231
? ? Swift 3 中的奇怪結果表明,如果數字溢出,它直接從0開始。這個例子中,999 % 2^8 = 231。
? ? Swift 4解決了這個問題,只有當數字可以被安全轉換時,才做可選類型轉換。
? ? Swift包管理器
? ? 過去幾個月,Swift包管理器已經有一定數量的更新,其中大的變更包括:
? ? 1、根據分支或者提交哈希進行源碼以來
? ? 2、可接受包版本的更多控制
? ? 3、替換不直觀的命令,使用一些更常用的解決模式
? ? 4、使用編譯器定義Swift版本的能力
? ? 5、為每個target指定源文件路徑
? ? 這些大變化都是Swift包管理器需要做的,SPM還有很長的路要走,我們都可以通過提案,幫助它更好發展。
? ? 一直在路上
? ? 在寫這篇文章時,仍有15分已接受的提案在排隊。如果你想知道接下來會發生什么,查看Swift演化提案、選擇“Accepted”。
? ? 路怎么走,你們自己挑?
? ? Swift語言這幾年不斷發育成熟。提案進程和社區參與,使得大家能夠跟蹤語言變化,也使得我們每個人都可以直接影響語言的演化。
? ? 上面的 Swift 4 變化,我們終于發現ABI穩定就在下一個轉角。Swift升級的陣痛在變小。構建性能和工具都大幅度提升。在蘋果生態外使用Swift變得越發可行了。設想一下,我們離一個直觀的實現,還差一小部分的String重寫。
? ? Swift還會迎來很多改變。跟上每個變化的節奏,查看以下資源: