- 原文地址:What’s new in Swift 5.0
- 原文做者:Paul Hudson
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:iWeslie
- 校對者:DevMcryYu, swants
Swift 5.0 是 Swift 的下一個主要的 release,隨之而來的是 ABI 的穩定性,同時還實現了幾個關鍵的新功能,包括 raw string,將來的枚舉 case,Result
類型,檢查整數倍數等等。前端
Result
類型SE-0235 在標準庫中引入了全新的 Result
類型,它讓咱們可以更加方便清晰地在複雜的代碼中處理 error,例如異步 API。python
Swift 的 Result
類型是用枚舉實現的,其中包含了 success
和 failure
。它們二者都使用泛型,所以你能夠爲它們指定任意類型。可是 failure
必須遵循 Swift 的 Error
協議。android
爲了進一步演示 Result
,咱們能夠寫一個網絡請求函數來計算用戶有多少未讀消息。在此示例代碼中,咱們將只有一個可能的錯誤,即請求的字符串不是有效的 URL:ios
enum NetworkError: Error {
case badURL
}
複製代碼
fetch 函數將接受 URL 字符串做爲其第一個參數,並將 completion 閉包做爲其第二個參數。該 completion 閉包自己將接受一個 Result
,其中 success 將存儲一個整數,failure 將是某種 NetworkError
。咱們實際上並無在這裏鏈接到服務器,但使用 completion 閉包可讓咱們模擬異步代碼。git
代碼以下:github
import Foundation
func fetchUnreadCount1(from urlString: String, completionHandler: @escaping (Result<Int, NetworkError>) -> Void) {
guard let url = URL(string: urlString) else {
completionHandler(.failure(.badURL))
return
}
// 此處省略複雜的網絡請求
print("Fetching \(url.absoluteString)...")
completionHandler(.success(5))
}
複製代碼
要調用此函數,咱們須要檢查 Result
中的值來看看咱們的請求是成功仍是失敗,代碼以下:正則表達式
fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
switch result {
case .success(let count):
print("\(count) 個未讀信息。")
case .failure(let error):
print(error.localizedDescription)
}
}
複製代碼
在開始在本身的代碼中使用 Result
以前,你還有三件事應該知道。express
首先,Result
有一個 get()
方法,若是存在則返回成功值,不然拋出錯誤。這容許你將 Result
轉換爲常規會拋出錯誤的函數調用,以下所示:swift
fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
if let count = try? result.get() {
print("\(count) 個未讀信息。")
}
}
複製代碼
其次,Result
還有一個接受拋出錯誤閉包的初始化器:若是閉包返回一個成功的值,用於 success
的狀況,不然拋出的錯誤則被傳入 failure
。後端
舉例:
let result = Result { try String(contentsOfFile: someFile) }
複製代碼
第三,你可使用通用的 Error
協議而不是你建立的特定錯誤的枚舉。實際上,Swift Evolution 提議說道「預計 Result 的大部分用法都會使用 Swift.Error
做爲 Error
類型參數。」
所以你要用 Result <Int,Error>
而非 Result<Int, NetworkError>
。這雖然意味着你失去了可拋出錯誤類型的安全性,但你能夠拋出各類不一樣的錯誤枚舉,其實這取決於你的代碼風格。
SE-0200 添加了建立原始字符串(raw string)的功能,其中反斜槓和井號是被做爲標點符號而不是轉義字符或字符串終止符。這使得許多用法變得更容易,特別是正則表達式。
要使用原始字符串,請在字符串前放置一個或多個 #
,以下所示:
let rain = #"西班牙"下的"雨"主要落在西班牙人的身上。"#
複製代碼
字符串開頭和結尾的 #
成爲字符串分隔符的一部分,所以 Swift 明白 "雨" 和 "西班牙" 兩邊獨立引號應該被視爲標點符號而不是終止符。
原始字符串也容許你使用反斜槓:
let keypaths = #"諸如 \Person.name 之類的 Swift keyPath 包含對屬性未調用的引用。"#
複製代碼
這將反斜槓視爲字符串中的文字字符而不是轉義字符。否則則意味着字符串插值的工做方式不一樣:
let answer = 42
let dontpanic = #"生命、宇宙及萬事萬物的終極答案都是 \#(answer)."#
複製代碼
請注意我是如何使用 \#(answer)
來調用字符串插值的,通常 \(answer)
將被解釋爲 answer 字符串中的字符,因此當你想要在原始字符串中進行引用字符串插值時你必須添加額外的 #
。
Swift 原始字符串的一個有趣特性是在開頭和結尾使用井號,由於你通常不會一下使用多個井號。這裏很難提供一個很好的例子,由於它真的應該很是罕見,但請考慮這個字符串:個人狗叫了一下 "汪"#好狗狗。由於在井號以前沒有空格,Swift 看到 "#
會當即把它做爲字符串終止符。在這種狀況下,咱們須要將分隔符從 #"
改成 ##"
,以下所示:
let str = ##"個人狗叫了一下 "汪"#乖狗狗"##
複製代碼
注意末尾的井號數必須與開頭的一致。
原始字符串與 Swift 的多行字符串系統徹底兼容,只需使用 #"""
開始,而後以 """#
結束,以下所示:
let multiline = #"""
生命、
宇宙,
以及衆生的答案都是 \#(answer).
"""#
複製代碼
能在正則表達式中再也不大量使用反斜槓足以證實這頗有用。例如編寫一個簡單的正則表達式來查詢關鍵路徑,例如 \Person.name
,看起來像這樣:
let regex1 = "\\\\[A-Z]+[A-Za-z]+\\.[a-z]+"
複製代碼
多虧了原始字符串,咱們能夠只用原來一半的反斜槓就能夠編寫相同的內容:
let regex2 = #"\\[A-Z]+[A-Za-z]+\.[a-z]+"#
複製代碼
咱們仍然須要 一些 反斜槓,由於正則表達式也使用它們。
SE-0228 大幅改進了 Swift 的字符串插值系統,使其更高效、靈活,並創造了之前不可能實現的全新功能。
在最基本的形式中,新的字符串插值系統讓咱們能夠控制對象在字符串中的顯示方式。Swift 具備有助於調試的結構體的默認行爲,它打印結構體名稱後跟其全部屬性。可是若是你使用類的話就沒有這種行爲,或者想要格式化該輸出以使其面向用戶,那麼你可使用新的字符串插值系統。
例如,若是咱們有這樣的結構體:
struct User {
var name: String
var age: Int
}
複製代碼
若是咱們想爲它添加一個特殊的字符串插值,以便咱們整齊地打印用戶信息,咱們將使用一個新的 appendInterpolation()
方法爲 String.StringInterpolation
添加一個 extension。Swift 已經內置了幾個,而且用戶插值 類型,在這種狀況下須要 User
來肯定要調用哪一個方法。
在這種狀況下,咱們將添加一個實現,將用戶的名稱和年齡放入一個字符串中,而後調用其中一個內置的 appendInterpolation()
方法將其添加到咱們的字符串中,以下所示:
extension String.StringInterpolation {
mutating func appendInterpolation(_ value: User) {
appendInterpolation("我叫\(value.name),\(value.age)歲")
}
}
複製代碼
如今咱們能夠建立一個用戶並打印出他們的數據:
let user = User(name: "Guybrush Threepwood", age: 33)
print("用戶信息:\(user)")
複製代碼
這將打印 用戶信息:我叫 Guybrush Threepwood,33 歲,而使用自定義字符串插值它將打印 用戶信息:User(name: "Guybrush Threepwood", age: 33) 。固然,該功能與僅實現CustomStringConvertible
協議沒有什麼不一樣,因此讓咱們繼續使用更高級的用法。
你的自定義插值方法能夠根據須要使用任意數量的參數,標記的和未標記的。例如,咱們可使用各類樣式添加插值來打印數字,以下所示:
extension String.StringInterpolation {
mutating func appendInterpolation(_ number: Int, style: NumberFormatter.Style) {
let formatter = NumberFormatter()
formatter.numberStyle = style
if let result = formatter.string(from: number as NSNumber) {
appendLiteral(result)
}
}
}
複製代碼
NumberFormatter
類有許多樣式,包括貨幣形式(489.00 元),序數形式(第一,第十二)和朗讀形式(五, 四十三)。 所以,咱們能夠建立一個隨機數,並將其拼寫成以下字符串:
let number = Int.random(in: 0...100)
let lucky = "這周的幸運數是 \(number, style: .spellOut)."
print(lucky)
複製代碼
你能夠根據須要屢次調用 appendLiteral()
,若是須要的話甚至能夠不調用。例如咱們能夠添加一個字符串插值來屢次重複一個字符串,以下所示:
extension String.StringInterpolation {
mutating func appendInterpolation(repeat str: String, _ count: Int) {
for _ in 0 ..< count {
appendLiteral(str)
}
}
}
print("Baby shark \(repeat: "doo ", 6)")
複製代碼
因爲這些只是常規方法,你可使用 Swift 的所有功能。例如,咱們可能會添加一個將字符串數組鏈接在一塊兒的插值,但若是該數組爲空,則執行一個返回字符串的閉包:
extension String.StringInterpolation {
mutating func appendInterpolation(_ values: [String], empty defaultValue: @autoclosure () -> String) {
if values.count == 0 {
appendLiteral(defaultValue())
} else {
appendLiteral(values.joined(separator: ", "))
}
}
}
let names = ["Harry", "Ron", "Hermione"]
print("學生姓名:\(names, empty: "空").")
複製代碼
使用 @autoclosure
意味着咱們可使用簡單值或調用複雜函數做爲默認值,但除非 values.count
爲零,不然不會作任何事。
經過結合使用 ExpressibleByStringLiteral
和 ExpressibleByStringInterpolation
協議,咱們如今可使用字符串插值建立整個類型,若是咱們添加 CustomStringConvertible
,只要咱們想要的話,甚至能夠將這些類型打印爲字符串。
爲了讓它生效,咱們須要知足一些特定的標準:
ExpressibleByStringLiteral
,ExpressibleByStringInterpolation
和 CustomStringConvertible
。只有在你想要自定義打印類型的方式時才須要遵循最後一個協議。StringInterpolation
並遵循 StringInterpolationProtocol
的嵌套結構體。appendLiteral()
方法,以及一個或多個 appendInterpolation()
方法。咱們能夠將全部這些放在一個能夠從各類常見元素構造 HTML 的示例類型中。嵌套 StringInterpolation
結構體中的 「暫存器」 將是一個字符串:每次添加新的文字或插值時,咱們都會將其追加到字符串的末尾。爲了讓你確切瞭解其中發生了什麼,我在各類追加方法中添加了一些 print()
來打印。
如下是代碼:
struct HTMLComponent: ExpressibleByStringLiteral, ExpressibleByStringInterpolation, CustomStringConvertible {
struct StringInterpolation: StringInterpolationProtocol {
// 以空字符串開始
var output = ""
// 分配足夠的空間來容納雙倍文字的文本
init(literalCapacity: Int, interpolationCount: Int) {
output.reserveCapacity(literalCapacity * 2)
}
// 一段硬編碼的文本,只需添加它就能夠
mutating func appendLiteral(_ literal: String) {
print("追加 ‘\(literal)’")
output.append(literal)
}
// Twitter 用戶名,將其添加爲連接
mutating func appendInterpolation(twitter: String) {
print("追加 ‘\(twitter)’")
output.append("<a href=\"https://twitter/\(twitter)\">@\(twitter)</a>")
}
// 電子郵件地址,使用 mailto 添加
mutating func appendInterpolation(email: String) {
print("追加 ‘\(email)’")
output.append("<a href=\"mailto:\(email)\">\(email)</a>")
}
}
// 整個組件的完整文本
let description: String
// 從文字字符串建立實例
init(stringLiteral value: String) {
description = value
}
// 從插值字符串建立實例
init(stringInterpolation: StringInterpolation) {
description = stringInterpolation.output
}
}
複製代碼
咱們如今可使用字符串插值建立和使用 HTMLComponent
的實例,以下所示:
let text: HTMLComponent = "你應該在 Twitter 上關注我 \(twitter: "twostraws"),或者你能夠發送電子郵件給我 \(email: "paul@hackingwithswift.com")。"
print(text)
複製代碼
多虧了分散在裏面的 print()
,你會看到字符串插值功能的準確做用:「追加 ‘你應該在 Twitter 上關注我’」,「追加 ’twostraws’」,「追加 ’,或者你能夠發送電子郵件給我 ’」,「追加 ’paul@hackingwithswift.com’」,最後 「追加 ’。’」,每一個部分觸發一個方法調用,並添加到咱們的字符串中。
SE-0216 爲 Swift 添加了一個新的 @dynamicCallable
註解,它讓一個類型能被直接調用。它是語法糖,而不是任何類型的編譯器的魔法,它把如下這段代碼:
let result = random(numberOfZeroes: 3)
複製代碼
轉換爲:
let result = random.dynamicallyCall(withKeywordArguments: ["numberOfZeroes": 3])
複製代碼
以前有一篇關於 Swift 中的動態特性 的文章裏有提到了動態查找成員(@dynamicMemberLookup)。@dynamicCallable
是 @dynamicMemberLookup
的天然擴展,它能使 Swift 代碼更容易與 Python 和 JavaScript 等動態語言一塊兒工做。
要將此功能添加到你本身的類型,你須要添加 @dynamicCallable
註解以及這些方法中的一個或兩個:
func dynamicallyCall(withArguments args: [Int]) -> Double
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double
複製代碼
第一個用於調用不帶參數標籤的類型(例如 a(b, c)
),第二個用於你 提供標籤 時(例如 a(b: cat, c: dog)
)。
@dynamicCallable
對於其方法接受和返回的數據類型很是靈活,它使你能夠從 Swift 的全部類型安全性中受益,同時具備一些高級用法。所以,對於第一種方法(無參數標籤),你可使用遵循了 ExpressibleByArrayLiteral
的任何東西,例如數組、數組切片和集合,對於第二種方法(使用參數標籤),你可使用遵循 ExpressibleByDictionaryLiteral
的任何東西。例如字典和鍵值對。
除了接受各類輸入外,你還能夠爲各類輸出提供多個重載,可能返回一個字符串、整數等等。只要 Swift 能推出使用哪個,你就能夠混合搭配你想要的一切。
咱們來看一個例子。首先,這是一個 RandomNumberGenerator
結構體,它根據傳入的輸入生成介於 0 和某個最大值之間的數字:
struct RandomNumberGenerator {
func generate(numberOfZeroes: Int) -> Double {
let maximum = pow(10, Double(numberOfZeroes))
return Double.random(in: 0...maximum)
}
}
複製代碼
要把它切換到 @dynamicCallable
,咱們會寫這樣的代碼:
@dynamicCallable
struct RandomNumberGenerator {
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double {
let numberOfZeroes = Double(args.first?.value ?? 0)
let maximum = pow(10, numberOfZeroes)
return Double.random(in: 0...maximum)
}
}
複製代碼
你能夠傳任意數量的參數甚至不傳參數來調用該方法,所以咱們當心讀取第一個值並結合是否爲 nil 的判斷來確保存在合理的默認值。
咱們如今能夠建立一個 RandomNumberGenerator
實例並像函數同樣調用它:
let random = RandomNumberGenerator()
let result = random(numberOfZeroes: 0)
複製代碼
若是你曾經使用過 dynamicallyCall(withArguments:)
,或者同時使用,由於你可讓它們都是單一類型,就能夠寫如下代碼:
@dynamicCallable
struct RandomNumberGenerator {
func dynamicallyCall(withArguments args: [Int]) -> Double {
let numberOfZeroes = Double(args[0])
let maximum = pow(10, numberOfZeroes)
return Double.random(in: 0...maximum)
}
}
let random = RandomNumberGenerator()
let result = random(0)
複製代碼
使用 @dynamicCallable
時須要注意一些重要的規則:
withKeywordArguments:
而且沒有實現 withArguments:
,你的類型仍然能夠在沒有參數標籤的狀況下調用,你只須要爲鍵得到空字符串。withKeywordArguments:
或 withArguments:
的實現被標記爲 throw,則調用該類型也將可拋出。@dynamicCallable
添加到 extension 裏,只可在類的主體裏面添加。也許更重要的是,不支持方法決議,這意味着咱們必須直接調用類型(例如 random(numberOfZeroes: 5)
)而不是調用類型上的特定方法(例如 random.generate(numberOfZeroes: 5)
)。已經有一些關於使用方法簽名添加後者的討論,例如:
func dynamicallyCallMethod(named: String, withKeywordArguments: KeyValuePairs<String, Int>)
複製代碼
若是那在將來的 Swift 版本中可能實現,它可能會爲 test mock 創造出一些很是有趣的可能性。
與此同時 @dynamicCallable
不太可能廣受歡迎,但對於但願與 Python,JavaScript 和其餘語言交互的少數人來講,它很是重要。
SE-0192 增長了在固定的枚舉和可能將被改變的枚舉間的區分度。
Swift 的一個安全特性是它要求全部 switch 語句都是詳盡的,它們必須覆蓋全部狀況。雖然這從安全角度來看效果很好,可是在未來添加新案例時會致使兼容性問題:系統框架可能會發送你未提供的不一樣內容,或者你依賴的代碼可能會添加新案例並致使你的編譯中斷,由於你的 switch 再也不詳盡。
使用 @unknown
註解,咱們如今能夠區分兩個略有不一樣的場景:「這個默認狀況應該針對全部其餘狀況運行,由於我不想單獨處理它們」 和 「我想單獨處理全部狀況,但若是未來出現任何問題,請使用此而非報錯。」
如下是一個枚舉示例:
enum PasswordError: Error {
case short
case obvious
case simple
}
複製代碼
咱們可使用 switch
編寫代碼來處理每一個案例:
func showOld(error: PasswordError) {
switch error {
case .short:
print("Your password was too short.")
case .obvious:
print("Your password was too obvious.")
default:
print("Your password was too simple.")
}
}
複製代碼
對於短密碼和弱強度密碼,它使用兩個 case,但將第三種狀況將會到 default 中處理。
如今若是未來咱們在 enum 中添加了一個名爲 old
的新 case,對於之前使用過的密碼,咱們的 default
case 會被自動調用,即便它的消息沒有意義。
Swift 沒法向咱們發出有關此代碼的警告,由於它在語法上沒有問題,所以很容易錯過這個錯誤。幸運的是,新的 @unknown
註解完美地修復了它,它只能用於 default
狀況,而且設計爲在未來出現新案例時能夠運行。
例如:
func showNew(error: PasswordError) {
switch error {
case .short:
print("Your password was too short.")
case .obvious:
print("Your password was too obvious.")
@unknown default:
print("Your password wasn't suitable.")
}
}
複製代碼
該代碼如今將產生警告,由於 switch
塊再也不詳盡,Swift 是但願咱們明確處理每一個 case 的。實際上這只是一個 警告,這使得這個屬性很實用:若是一個框架在將來添加一個新 case,你將獲得警告,但它不會讓你的代碼編譯不經過。
SE-0230 修改 try?
的工做方式,以便嵌套的可選項被展平成爲一個常規的選擇。這使得它的工做方式與可選鏈和條件類型轉換(if let)的工做方式相同,這兩種方法都在早期的 Swift 版本中展平了可選項。
這是一個演示變化的示例:
struct User {
var id: Int
init?(id: Int) {
if id < 1 {
return nil
}
self.id = id
}
func getMessages() throws -> String {
// 複雜的一段代碼
return "No messages"
}
}
let user = User(id: 1)
let messages = try? user?.getMessages()
複製代碼
User
結構體有一個可用的初始化器,由於咱們想確保開發者建立具備有效 ID 的用戶。getMessages()
方法理論上包含某種複雜的代碼來獲取用戶的全部消息列表,所以它被標記爲 throws
,我已經讓它返回一個固定的字符串,因此代碼可編譯經過。
關鍵在於最後一行:由於用戶是可選的而使用可選鏈,由於 getMessages()
能夠拋出錯誤,它使用 try?
將 throw 方法轉換爲可選的,因此咱們最終獲得一個嵌套的可選。在 Swift 4.2 和更早版本中,這將使 messages
成爲 String??
,一個可選的可選字符串,可是在 Swift 5.0 和更高版本中 try?
若是對於已是可選的類型,它們不會將值包裝成可選類型,因此 messages
將只是一個 String?
。
此新行爲與可選鏈和條件類型轉換(if let)的現有行爲相匹配。也就是說,若是你須要的話,能夠在一行代碼中使用可選鏈十幾回,但最終不會有那麼多個嵌套的可選。相似地,若是你使用 as?
的可選鏈,你仍然只有一個級別的可選性,而這一般是你想要的。
SE-0225 爲整數添加 isMultiple(of:)
方法來容許咱們以比使用取餘數運算 %
更清晰的方式檢查一個數是不是另外一個數的倍數。
例如:
let rowNumber = 4
if rowNumber.isMultiple(of: 2) {
print("Even")
} else {
print("Odd")
}
複製代碼
沒錯,咱們可使用 if rowNumber % 2 == 0
實現相同的功能,但你不得不認可這樣看起來不清晰,使用 isMultiple(of:)
意味着它能夠在 Xcode 的代碼自動補全中列出,這有助於你發現。
SE-0218 爲字典添加了一個新的 compactMapValues()
方法,它可以將數組中的 compactMap()
功能轉換我須要的值,解包結果,而後丟棄任何 nil,與字典中的 mapValues()
方法一塊兒使用能保持鍵的完整並只轉換值。
舉個例子,這裏是一個比賽數據的字典,以及他們完成的秒數。其中有一我的沒有完成,標記爲 「DNF」(未完成):
let times = [
"Hudson": "38",
"Clarke": "42",
"Robinson": "35",
"Hartis": "DNF"
]
複製代碼
咱們可使用 compactMapValues()
建立一個名字和時間爲整數的新字典,刪除一個 DNF 的人:
let finishers1 = times.compactMapValues { Int($0) }
複製代碼
或者你能夠直接將 Int
初始化器傳遞給 compactMapValues()
,以下所示:
let finishers2 = times.compactMapValues(Int.init)
複製代碼
你還可使用 compactMapValues()
來展開選項並丟棄 nil 值而不執行任何類型轉換,以下所示:
let people = [
"Paul": 38,
"Sophie": 8,
"Charlotte": 5,
"William": nil
]
let knownAges = people.compactMapValues { $0 }
複製代碼
這個 Swift 5.0 功能在 beta 版中被撤銷,由於它致使了類型檢查器的性能問題。但願它可以在 Swift 5.1 迴歸,或者用一個新名稱來避免問題。
SE-0220 引入了一個新的 count(where:)
方法,該方法執行 filter()
的等價方法並在一次傳遞中計數。這樣能夠節省當即丟棄的新陣列的建立,併爲常見問題提供清晰簡潔的解決方案。
此示例建立一個測試結果數組,並計算大於或等於 85 的數的個數:
let scores = [100, 80, 85]
let passCount = scores.count { $0 >= 85 }
複製代碼
這計算了數組中有多少名稱以 「Terry」 開頭:
let pythons = ["Eric Idle", "Graham Chapman", "John Cleese", "Michael Palin", "Terry Gilliam", "Terry Jones"]
let terryCount = pythons.count { $0.hasPrefix("Terry") }
複製代碼
全部遵循 Sequence
的類型均可以使用此方法,所以你也能夠在集合和字典上使用它。
Swift 5.0 是 Swift 的最新版本,但以前的版本也包含了不少功能。你能夠閱讀如下文章:
但還有更多,蘋果已經在 Swift.org 上宣佈了 Swift 5.1發佈流程,其中包括模塊穩定性以及其餘一些改進。在撰寫本文時,5.1 的附加條款不多,但看起來咱們會看到它在 WWDC 附近發佈。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。