- 原文地址: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
型別,檢查整數倍數等等。
- 你可以親自嘗試一下:我建立了一個 Xcode Playground 來展示 Swift 5.0 的新特性,裡面有一些你可以參考的例子。
標準 Result
型別
SE-0235 在標準庫中引入了全新的 Result
型別,它讓我們能夠更加方便清晰地在複雜的程式碼中處理 error,例如非同步 API。
Swift 的 Result
型別是用列舉實現的,其中包含了 success
和 failure
。它們兩者都使用泛型,因此你可以為它們指定任意型別。但是 failure
必須遵循 Swift 的 Error
協議。
為了進一步演示 Result
,我們可以寫一個網路請求函式來計算使用者有多少未讀訊息。在此示例程式碼中,我們將只有一個可能的錯誤,即請求的字串不是有效的 URL:
enum NetworkError: Error {
case badURL
}
複製程式碼
fetch 函式將接受 URL 字串作為其第一個引數,並將 completion 閉包作為其第二個引數。該 completion 閉包本身將接受一個 Result
,其中 success 將儲存一個整數,failure 將是某種 NetworkError
。我們實際上並沒有在這裡連線到伺服器,但使用 completion 閉包可以讓我們模擬非同步程式碼。
程式碼如下:
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
之前,你還有三件事應該知道。
首先,Result
有一個 get()
方法,如果存在則返回成功值,否則丟擲錯誤。這允許你將 Result
轉換為常規會丟擲錯誤的函式呼叫,如下所示:
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>
。這雖然意味著你失去了可丟擲錯誤型別的安全性,但你可以丟擲各種不同的錯誤列舉,其實這取決於你的程式碼風格。
Raw string
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]+"#
複製程式碼
我們仍然需要 一些 反斜槓,因為正規表示式也使用它們。
Customizing string interpolation
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’”,最後 “追加 ’。’”,每個部分觸發一個方法呼叫,並新增到我們的字串中。
動態可呼叫(dynamicCallable)型別
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 和其他語言互動的少數人來說,它非常重要。
面向未來的列舉 case
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,你將得到警告,但它不會讓你的程式碼編譯不通過。
try? 巢狀可選的展平
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?
的可選鏈,你仍然只有一個級別的可選性,而這通常是你想要的。
Integer 整倍數自省
SE-0225 為整數新增 isMultiple(of:)
方法來允許我們以比使用取餘數運算 %
更清晰的方式檢查一個數是否是另一個數的倍數。
例如:
let rowNumber = 4
if rowNumber.isMultiple(of: 2) {
print("Even")
} else {
print("Odd")
}
複製程式碼
沒錯,我們可以使用 if rowNumber % 2 == 0
實現相同的功能,但你不得不承認這樣看起來不清晰,使用 isMultiple(of:)
意味著它可以在 Xcode 的程式碼自動補全中列出,這有助於你發現。
使用 compactMapValues() 轉換和解包字典值
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、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。