[譯] Swift 5.0 新特性

iWeslie發表於2019-04-03

Swift 5.0 是 Swift 的下一個主要的 release,隨之而來的是 ABI 的穩定性,同時還實現了幾個關鍵的新功能,包括 raw string,未來的列舉 case,Result 型別,檢查整數倍數等等。

  • 你可以親自嘗試一下:我建立了一個 Xcode Playground 來展示 Swift 5.0 的新特性,裡面有一些你可以參考的例子。

標準 Result 型別

SE-0235 在標準庫中引入了全新的 Result 型別,它讓我們能夠更加方便清晰地在複雜的程式碼中處理 error,例如非同步 API。

Swift 的 Result 型別是用列舉實現的,其中包含了 successfailure。它們兩者都使用泛型,因此你可以為它們指定任意型別。但是 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 為零,否則不會做任何事。

通過結合使用 ExpressibleByStringLiteralExpressibleByStringInterpolation 協議,我們現在可以使用字串插值建立整個型別,如果我們新增 CustomStringConvertible,只要我們想要的話,甚至可以將這些型別列印為字串。

為了讓它生效,我們需要滿足一些特定的標準:

  • 我們建立的型別應該遵循 ExpressibleByStringLiteralExpressibleByStringInterpolationCustomStringConvertible。只有在你想要自定義列印型別的方式時才需要遵循最後一個協議。
  • 在你的型別 內部 需要是一個名為 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 連結。


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

[譯] Swift 5.0 新特性

相關文章