Swift 4.0 新特性
WWDC 2017 帶來了很多驚喜,在這次大會上,Swift 4 也伴隨著 Xcode 9 測試版來到了我們的麵前,雖然正式版要8月底9月初才會公布,但很多強大的新特性正吸引我們去學習它。根據大會上已經開放的新特性,先一睹為快。
體驗
Swift 4包含在Xcode 9中,您可以從Apple的開發者門戶下載最新版本的Xcode 9(您必須擁有一個活躍的開發者帳戶)。 每個Xcode測試版將在發布時捆綁最新的Swift 4快照。在閱讀時,您會注意到[SE-xxxx]格式的鏈接。 這些鏈接將帶您到相關的Swift Evolution提案。 如果您想了解有關任何主題的更多信息,請務必查看。
版本遷移
由於Swift 4新增了很多的新的語法特性,這些語法和思想完全區別於Swift 3及以下版本。因此,使用Swift遷移工具將為您處理大部分更改,在Xcode中,您可以導航到編輯/轉換/到當前Swift語法…以啟動轉換工具。
語法改進
extension 中可以訪問 private 的屬性
例如有如下代碼:
struct Date: Equatable, Comparable {
private let secondsSinceReferenceDate: Double
static func ==(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate == rhs.secondsSinceReferenceDate
}
static func <(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate < rhs.secondsSinceReferenceDate
}
}
上麵代碼定義了一個 Date 結構體,並實現 Equatable 和 Comparable 協議。為了讓代碼更清晰,可讀性更好,一般會把對協議的實現放在單獨的 extension 中,這也是一種非常符合 Swift 風格的寫法。
struct Date {
private let secondsSinceReferenceDate: Double
}
extension Date: Equatable {
static func ==(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate == rhs.secondsSinceReferenceDate
}
}
extension Date: Comparable {
static func <(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate < rhs.secondsSinceReferenceDate
}
}
但是在 Swift 3 中,編譯就報錯了,因為 extension 中無法獲取到 secondsSinceReferenceDate 屬性,因為它是 private 的。所以,在 Swift 3 中必須把 private 改為 fileprivate。但是如果用 fileprivate,屬性的作用域就會更大,可能會不小心造成屬性的濫用。
struct Date {
fileprivate let secondsSinceReferenceDate: Double
}
...
而在 Swift 4 中,private 的屬性的作用域擴大到了 extension 中,並且被限定在了 struct 和 extension 內部,這樣就不需要再改成 fileprivate 了。
類型和協議的組合類型
考慮以下如下代碼:
protocol Shakeable {
func shake()
}
extension UIButton: Shakeable { /* ... */ }
extension UISlider: Shakeable { /* ... */ }
func shakeEm(controls: [???]) {
for control in controls where control.state.isEnabled {
}
control.shake()
}
???處怎麼寫呢?在Swift 3中可以這麼寫。
func shakeEm(controls: [UIControl]) {
for control in controls where control.isEnabled {
if control is Shakeable {
(control as! Shakeable).shake()
}
}
}
在Swift 4中,如果將類型和協議用 & 組合在一起使用,代碼就可以這麼寫了。
protocol Shakeable {
func shake()
}
extension UIButton: Shakeable { /* ... */ }
extension UISlider: Shakeable { /* ... */ }
func shakeEm(controls: [UIControl & Shakeable]) {
for control in controls where control.state.isEnabled {
control.shake()
}// Objective-C API
@interface NSCandidateListTouchBarItem<CandidateType> : NSTouchBarItem
@property (nullable, weak) NSView <NSTextInputClient> *client;
@end
}
Associated Type 追加Where 約束語句
在 Swift 4 中可以在 associated type 後麵聲明的類型後追加 where 語句,其語法格式如下:
associatedtype Element where <xxx>
下麵是 Swift 4 標準庫中 Sequence 中 Element 的聲明:
protocol Sequence {
associatedtype Element where Self.Element == Self.Iterator.Element
// ...
}
它限定了 Sequence 中 Element 這個類型必須和 Iterator.Element 的類型一致。通過 where 語句可以對類型添加更多的約束,使其更嚴謹,避免在使用這個類型時做多餘的類型判斷。
Key Paths 語法
先來看看Swift 3的Key Paths語法:
@objcMembers class Kid: NSObject {
dynamic var nickname: String = ""
dynamic var age: Double = 0.0
dynamic var friends: [Kid] = []
}
var ben = Kid(nickname: "Benji", age: 5.5)
let kidsNameKeyPath = #keyPath(Kid.nickname)
let name = ben.valueForKeyPath(kidsNameKeyPath)
ben.setValue("Ben", forKeyPath: kidsNameKeyPath)
在Swift 4中上麵的代碼可以這樣寫:
struct Kid {
var nickname: String = ""
var age: Double = 0.0
var friends: [Kid] = []
}
var ben = Kid(nickname: "Benji", age: 8, friends: [])
let name = ben[keyPath: \Kid.nickname]
ben[keyPath: \Kid.nickname] = "BigBen"
相比 Swift 3,Swift 4 的 Key Paths 具有以下優勢:
- 類型可以定義為 class、struct;
- 定義類型時無需加上 @objcMembers、dynamic 等關鍵字;
- 性能更好;
- 類型安全和類型推斷,例如 ben.valueForKeyPath(kidsNameKeyPath) 返回的類型是 Any,ben[keyPath: \Kid.nickname] 直接返回 String 類型;
- 可以在所有值類型上使用;
下標支持泛型
Swift 支持通過下標來讀寫容器中的數據,但是如果容器類中的數據類型定義為泛型,以前的下標語法就隻能返回 Any,在取出值後需要用 as? 來轉換類型。在Swift 4中,下標也可以使用泛型。
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
}
}
let dictionary = GenericDictionary(data: ["Name": "Xiaoming"])
let name: String? = dictionary["Name"] // 不需要再寫 as? String
字符串
Unicode 字符串
在 Unicode 中,有些字符是由幾個其它字符組成的,比如 é 這個字符,它可以用 \u{E9} 來表示,也可以用 e 字符和上麵一撇字符組合在一起表示 \u{65}\u{301}。例如:
這個 family 是一個由多個字符組合成的字符,打印出來的結果為 一個家庭。上麵的代碼在 Swift 3 中打印的 count 數是 4,在 Swift 4 中打印出的 count 是 1。
更快的字符處理速度
Swift 4 的字符串優化了底層實現,對於英語、法語、德語、西班牙語的處理速度提高了 3.5 倍。對於簡體中文、日語的處理速度提高了 2.5 倍。
去掉了 characters
Swift 3 中的 String 需要通過 characters 去調用的屬性方法,在 Swift 4 中可以通過 String 對象本身直接調用,例如:
let values = "one,two,three..."
var i = values.characters.startIndex
while let comma = values.characters[i...<values.characters.endIndex].index(of: ",") {
if values.characters[i..<comma] == "two" {
print("found it!")
}
i = values.characters.index(after: comma)
}
在Swift 4 可以把上麵代碼中的所有的 characters 都去掉:
let values = "one,two,three..."
var i = values.startIndex
while let comma = values[i...<values.endIndex].index(of: ",") {
if values[i..<comma] == "two" {
print("found it!")
}
i = values.index(after: comma)
}
One-sided Slicing
Swift 4 新增了一個語法糖 ... 可以對字符串進行單側邊界取子串。例如:
let values = "abcdefg"
let startSlicingIndex = values.index(values.startIndex, offsetBy: 3)
let subvalues = values[startSlicingIndex...] // One-sided Slicing
// defg
將String 當做 Collection 來用
Swift 4 中 String 可以當做 Collection 來用,並不是因為 String 實現了 Collection 協議,而是 String 本身增加了很多 Collection 協議中的方法,使得 String 在使用時看上去就是個 Collection。例如:
翻轉字符串
let abc: String = "abc"
print(String(abc.reversed()))
// cba
遍曆字符
let abc: String = "abc"
for c in abc {
print(c)
}
/*
a
b
c
*/
Map、Filter、Reduce
// map
let abc: String = "abc"
_ = abc.map {
print($0.description)
}
// filter
let filtered = abc.filter { $0 == "b" }
// reduce
let result = abc.reduce("1") { (result, c) -> String in
print(result)
print(c)
return result + String(c)
}
print(result)
Substring
在 Swift 中,String 的背後有個 Owner Object 來跟蹤和管理這個 String,String 對象在內存中的存儲由內存其實地址、字符數、指向 Owner Object 指針組成。Owner Object 指針指向 Owner Object 對象,Owner Object 對象持有 String Buffer。當對 String 做取子字符串操作時,子字符串的 Owner Object 指針會和原字符串指向同一個對象,因此子字符串的 Owner Object 會持有原 String 的 Buffer。當原字符串銷毀時,由於原字符串的 Buffer 被子字符串的 Owner Object 持有了,原字符串 Buffer 並不會釋放,造成極大的內存浪費。
在 Swift 4 中,做取子串操作的結果是一個 Substring 類型,它無法直接賦值給需要 String 類型的地方。必須用 String() 包一層,係統會通過複製創建出一個新的字符串對象,這樣原字符串在銷毀時,原字符串的 Buffer 就可以完全釋放了。例如:
let big = downloadHugeString()
let small = extractTinyString(from: big)
mainView.titleLabel.text = small // Swift 4 編譯報錯
mainView.titleLabel.text = String(small) // 編譯通過
多行字符串字麵量
Swift 3 中寫很長的字符串隻能寫在一行。
func tellJoke(name: String, character: Character) {
let punchline = name.filter { $0 != character }
let n = name.count - punchline.count
let joke = "Q: Why does \(name) have \(n) \(character)'s in their name?\nA: I don't know, why does \(name) have \(n) \(character)'s in their name?\nQ: Because otherwise they'd be called \(punchline)."
print(joke)
}
tellJoke(name: "Edward Woodward", character: "d")
字符串中間有換行隻能通過添加 \n 字符來代表換行。Swift 4 可以把字符串寫在一對 """ 中,這樣字符串就可以寫成多行。
func tellJoke(name: String, character: Character) {
let punchline = name.filter { $0 != character }
let n = name.count - punchline.count
let joke = """
Q: Why does \(name) have \(n) \(character)'s in their name?
A: I don't know, why does \(name) have \(n) \(character)'s in their name?
Q: Because otherwise they'd be called \(punchline).
"""
print(joke)
}
tellJoke(name: "Edward Woodward", character: "d")
Swift 標準庫
Encoding and Decoding
當需要將一個對象持久化時,需要把這個對象序列化,往常的做法是實現 NSCoding 協議,寫過的人應該都知道實現 NSCoding 協議的代碼寫起來很痛苦,尤其是當屬性非常多的時候。Swift 4 中引入了 Codable 幫我們解決了這個問題,這和Java等麵向對象語言有異曲同工之妙。例如:
struct Language: Codable {
var name: String
var version: Int
}
想讓這個 Language 對象的實例持久化,隻需要讓 Language 符合 Codable 協議即可,Language 中不用寫別的代碼。符合了 Codable 協議以後,可以選擇把對象 encode 成 JSON 或者 PropertyList。
Encode操作
let swift = Language(name: "Swift", version: 4)
if let encoded = try? JSONEncoder().encode(swift) {
// 把 encoded 保存起來
}
Decode操作
if let decoded = try? JSONDecoder().decode(Language.self, from: encoded) {
print(decoded.name)
}
Sequence
在Swift 3中,
protocol Sequence {
associatedtype Iterator: IteratorProtocol
func makeIterator() -> Iterator
}
由於 Swift 4 中的 associatedtype 支持追加 where 語句,所以 Sequence 做了這樣的改進。
protocol Sequence {
associatedtype Element
associatedtype Iterator: IteratorProtocol where Iterator.Element == Element
func makeIterator() -> Iterator
}
Swift 4 中獲取 Sequence 的元素類型可以不用 Iterator.Element,而是直接取 Element。例如:
protocol Sequence {
associatedtype SubSequence: Sequence
where SubSequence.SubSequence == SubSequence,
SubSequence.Element == Element
}
通過 where 語句的限定,保證了類型正確,避免在使用 Sequence 時做一些不必要的類型判斷。
Protocol-oriented integers
整數類型的協議也做了修改,新增了 FixedWidthInteger 等協議,具體的協議繼承關係如下:
Dictionary and Set enhancements
這裏簡單的羅列了Dictionary 和 Set 增強的功能點:
- 通過 Sequence 來初始化;
- 可以包含重複的 Key
- Filter 的結果的類型和原類型一致
- Dictionary 的 mapValues 方法
- Dictionary 的默認值
- Dictionary 可以分組
- Dictionary 可以翻轉
NSNumber
在 Swift 4 中,把一個值為 999 的 NSNumber 轉換為 UInt8 後,能正確的返回 nil,而在 Swift 3 中會不可預料的返回 231。
let n = NSNumber(value: 999)
let v = n as? UInt8 // Swift 4: nil, Swift 3: 231
MutableCollection.swapAt(::)
MutableCollection 現在有了一個新方法 swapAt(::) 用來交換兩個位置的值,例如:
var mutableArray = [1, 2, 3, 4]
mutableArray.swapAt(1, 2)
print(mutableArray)
// 打印結果:[1, 3, 2, 4]
Xcode改進
New Build System
Xcode 9 引入了 New Build System,可在 Xcode 9 的 File -> Project Settings... 中選擇開啟。
預編譯 Bridging Headers 文件
對於 Swift 和 Objective-C 混合的項目,Swift 調用 Objective-C 時,需要建立一個 Bridging Headers 文件,然後把 Swift 要調用的 Objective-C 類的頭文件都寫在裏麵,編譯器會讀取 Bridging Headers 中的頭文件,然後生成一個龐大的 Swift 文件,文件內容是這些頭文件內的 API 的 Swift 版本。然後編譯器會在編譯每一個 Swift 文件時,都要編譯一遍這個龐大的 Swift 文件的內容。
有了預編譯 Bridging Headers 以後,編譯器會在預編譯階段把 Bridging Headers 編譯一次,然後插入到每個 Swift 文件中,這樣就大大提高了編譯速度(蘋果宣稱 Xcode 9 和 Swift 4 對於 Swift 和 Objective-C 混合編譯的速度提高了 40%)。
COW Existential Containers
Swift 中有個東西叫 Existential Containers,它用來保存未知類型的值,它的內部是一個 Inline value buffer,如果 Inline value buffer 中的值占用空間很大時,這個值會被分配在堆上,然而在堆上分配內存是一個性能比較慢的操作。
Swift 4 中為了優化性能引入了 COW Existential Containers,這裏的 COW 就代表 "Copy-On-Write",當存在多個相同的值時,他們會共用 buffer 上的空間,直到某個值被修改時,這個被修改的值才會被拷貝一份並分配內存空間。
移除未調用的協議實現
struct Date {
private let secondsSinceReferenceDate: Double
}
extension Date: Equatable {
static func ==(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate == rhs.secondsSinceReferenceDate
}
}
extension Date: Comparable {
static func <(lhs: Date, rhs: Date) -> Bool {
return lhs.secondsSinceReferenceDate < rhs.secondsSinceReferenceDate
}
}
例如,上麵的代碼中,Date 實現了 Equatable 和 Comparable 協議。編譯時如果編譯器發現沒有任何地方調用了對 Date 進行大小比較的方法,編譯器會移除 Comparable 協議的實現,來達到減小包大小的目的。
減少隱式 @objc 自動推斷
在項目中想把 Swift 寫的 API 暴露給 Objective-C 調用,需要增加 @objc。在 Swift 3 中,編譯器會在很多地方為我們隱式的加上 @objc,例如當一個類繼承於 NSObject,那麼這個類的所有方法都會被隱式的加上 @objc。這樣很多並不需要暴露給 Objective-C 也被加上了 @objc。大量 @objc 會導致二進製文件大小的增加。
class MyClass: NSObject {
func print() { ... } // 包含隱式的 @objc
func show() { ... } // 包含隱式的 @objc
}
在 Swift 4 中,隱式 @objc 自動推斷隻會發生在很少的當必須要使用 @objc 的情況,比如:
- 複寫父類的 Objective-C 方法
- 符合一個 Objective-C 的協議
其它大多數地方必須手工顯示的加上 @objc。減少了隱式 @objc 自動推斷後,Apple Music app 的包大小減少了 5.7%。
兼容
Xcode 9 中同時集成了 Swift 3.2 和 Swift 4。
- Swift 3.2 完全兼容 Swift 3.1,並會在過時的語法或函數上報告警告。
- Swift 3.2 具有 Swift 4 的一些寫法,但是性能不如 Swift 4。
- Swift 3.2 和 Swift 4 可以混合編譯,可以指定一部分模塊用 Swift 3.2 編譯,一部分用 Swift 4 編譯。
- 遷移到 Swift 4 後能獲得 Swift 4 所有的新特性,並且性能比 Swift 3.2 好。
當 Xcode 正式版發布後,現有的 Swift 代碼可以直接升級到 Swift 3.2 而不用做任何改動,後續可以再遷移到 Swift 4。或者直接遷移到 Swift 4 也可以,Swift 4 相比 Swift 3 的 API 變化還是不大的,很多第三方庫都可以直接用 Swift 4 編譯。Swift 1 到 2 和 Swift 2 到 3 的遷移的痛苦在 3 到 4 的遷移上已經大大改善了。
參考資料:
- https://github.com/apple/swift-evolution
- https://github.com/ole/whats-new-in-swift-4
- https://www.raywenderlich.com/163857/whats-new-swift-4
- https://www.hackingwithswift.com/swift4
最後更新:2017-08-15 22:32:36