探討SWIFT 5.2的新功能特性

從表面上看,SWIFT 5.2在新的語言特性方面確定是一個小版本,由於這個新版本的大部分重點是提升SWIFT底層基礎結構的速度和穩定性,例如如何報告編譯器錯誤,以及如何解決構建級依賴。面試

然而,斯威夫特5.2總數新的語言特性可能相對較小,它確實包括兩個新功能,它們可能會對SWIFT的總體功能產生至關大的影響。函數式程序設計語言.編程

本週,讓咱們探討這些特性,以及咱們如何可能使用它們來接受一些在函數式編程世界中很是流行的不一樣範例--在面向對象的SWIFT代碼庫中,它們可能會感受更加一致和熟悉。api

在咱們開始以前,做爲Xcode 11.4的一部分,SWIFT5.2仍然處於測試版,請注意,本文是一篇很是探索性的文章,表明了我對這些新語言特性的第一印象。隨着我在生產中使用新特性得到更多經驗,個人觀點可能會發生變化,儘管我將嘗試在這種狀況下更新這篇文章,但我建議您使用本文做爲靈感,親自探索這些新特性,而不是直接使用以原樣呈現的解決方案。緩存

有了這個小小的免責聲明,讓咱們開始探索吧!bash

做爲一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個個人iOS交流羣:1012951431 無論你是小白仍是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經驗,討論技術, 你們一塊兒交流學習成長!網絡

另附上一份各好友收集的大廠面試題,進羣可自行下載!閉包

調用類型爲函數

儘管SWIFT並非一種嚴格的函數式編程語言,但毫無疑問,函數在其整體設計和使用中扮演着很是重要的角色。從閉包如何做爲異步回調使用,到集合如何大量使用典型的函數模式(如map和reduce-職能無處不在。app

SWIFT5.2的有趣之處在於它開始模糊函數和類型之間的界限。儘管咱們一直可以將任何給定類型的實例方法做爲函數傳遞(由於SWIFT支持一級函數),咱們如今可以調用某些類型,就好像它們自己是函數同樣。.框架

讓咱們先來看看一個使用Cache咱們內置的類型「SWIFT中的緩存」-這提供了一個更多的「快速友好」包裝上的APINSCache:異步

class Cache<Key: Hashable, Value> {
    private let wrapped = NSCache<WrappedKey, Entry>()
    private let dateProvider: () -> Date
    private let entryLifetime: TimeInterval
    
    ...

    func insert(_ value: Value, forKey key: Key) {
        ...
    }
}
複製代碼

假設咱們想要向上面的類型添加一個方便的API--讓咱們自動使用插入值的id做爲它的緩存鍵,以防當前Value類型符合標準庫的Identifiable協議。雖然咱們能夠簡單地命名新的apiinsert還有,咱們要給它起一個很是特別的名字-callAsFunction:

extension Cache where Value: Identifiable, Key == Value.ID {
    func callAsFunction(_ value: Value) {
        insert(value, forKey: value.id)
    }
}
複製代碼

這彷佛是一種奇怪的命名約定,但經過這樣命名咱們的新方便方法,咱們實際上已經給出了Cache輸入一個有趣的新功能--它如今可能被稱爲函數--以下所示:

let document: Document = ...
let cache = Cache<Document.ID, Document>()

// We can now call our 'cache' variable as if it was referencing a
// function or a closure:
cache(document)
複製代碼

能夠說,這既很酷,也很奇怪。但問題是-它有什麼用呢?讓咱們繼續探索,看看DocumentRenderer協議,它爲用於呈現的各類類型定義了一個公共接口。Document應用程序中的實例:

protocol DocumentRenderer {
    func render(_ document: Document,
                in context: DocumentRenderingContext,
                enableAnnotations: Bool)
}
複製代碼

相似於咱們以前向咱們的Cache類型,讓咱們在這裏作一樣的事情-只是這一次,咱們將擴展上面的協議,以容許任何符合的類型被調用爲一個函數,其中包含一組默認參數:

extension DocumentRenderer {
    func callAsFunction(_ document: Document) {
        render(document,
            in: .makeDefaultContext(),
            enableAnnotations: false
        )
    }
}
複製代碼

上述兩個變化在孤立的狀況下看起來可能不那麼使人印象深入,可是若是咱們將它們放在一塊兒,咱們就能夠看到爲一些更復雜的類型提供基於功能的方便API的吸引力。例如,咱們在這裏構建了一個DocumentViewController-使用咱們的Cache類型,以及基於核心動畫的DocumentRenderer協議--在加載文檔時,這兩種協議如今均可以簡單地做爲函數調用:

class DocumentViewController: UIViewController {
    private let cache: Cache<Document.ID, Document>
    private let render: CoreAnimationDocumentRenderer
    
    ...

    private func documentDidLoad(_ document: Document) {
        cache(document)
        render(document)
    }
}
複製代碼

這很酷,特別是若是咱們的目標是輕量級API設計或者若是咱們在建造某種形式的領域專用語言。雖然經過傳遞實例方法來實現相似的結果一直是可能的好像它們是封閉的-經過容許直接調用咱們的類型,咱們都避免了手動傳遞這些方法,而且可以保留API可能使用的任何外部參數標籤。

例如,假設咱們還想作一個PriceCalculator變成一個可調用的類型。爲了維護原始API的語義,咱們將保留for外部參數標籤,即便在聲明callAsFunction執行狀況-以下:

extension PriceCalculator {
    func callAsFunction(for product: Product) -> Int {
        calculatePrice(for: product)
    }
}
複製代碼

下面是上述方法與存儲對類型的引用的比較calculatePrice方法-請注意第一段代碼是如何丟棄參數標籤的,而第二段代碼是如何保留參數標籤的:

// Using a method reference:
let calculatePrice = PriceCalculator().calculatePrice
...
calculatePrice(product)

// Calling our type directly:
let calculatePrice = PriceCalculator()
...
calculatePrice(for: product)
複製代碼

讓類型像函數同樣被調用是一個很是有趣的概念,但也許更有趣的是,它還使咱們可以走相反的方向--並將函數轉換爲適當的類型。

面向對象的函數式編程

雖然在許多函數式編程概念中有着巨大的威力,但當使用大量面向對象的框架(就像大多數Apple的框架同樣)時,應用這些概念和模式每每是頗有挑戰性的。讓咱們看看SWIFT5.2的新可調用類型功能是否能夠幫助咱們改變這種情況。

因爲咱們如今可使任何類型可調用,因此咱們還能夠將任何函數轉換爲類型,同時仍然容許像一般那樣調用該函數。爲了實現這一點,讓咱們定義一個名爲Function,看起來是這樣的:

struct Function<Input, Output> {
    let raw: (Input) -> Output

    init(_ raw: @escaping (Input) -> Output) {
        self.raw = raw
    }

    func callAsFunction(_ input: Input) -> Output {
        raw(input)
    }
}
複製代碼

就像咱們以前定義的可調用類型同樣,Function實例能夠直接調用,使得它們在大多數狀況下的行爲方式與它們的基本功能相同。

使不接受任何輸入的函數仍然被調用,而無需手動指定Void做爲一個參數,咱們還定義瞭如下擴展Function有Void做爲他們的Input類型:

extension Function where Input == Void {
    func callAsFunction() -> Output {
        raw(Void())
    }
}
複製代碼

上述包裝器類型的酷之處在於,它使咱們可以以更多面向對象的方式採用真正強大的函數式編程概念。讓咱們來看看兩個這樣的概念-部分適用和管系(咱們也用在SWIFT中的功能網絡)。前者容許咱們將一個函數與一個值組合起來,生成一個不須要任何輸入的新函數,然後者使咱們可以將兩個函數連接在一塊兒--如今能夠這樣實現:

extension Function {
    func combined(with value: Input) -> Function<Void, Output> {
        Function<Void, Output> { self.raw(value) }
    }
    
    func chained<T>(to next: @escaping (Output) -> T) -> Function<Input, T> {
        Function<Input, T> { next(self.raw($0)) }
    }
}
複製代碼

請注意,咱們是如何命名上述兩個函數的combined和chained爲了讓他們感受更多「在家」在SWIFT中,而不是使用一般在更嚴格的函數式編程語言中找到的名稱。

上面的設置使咱們可以使用如下技術基於函數的依賴注入以一種仍然感受很是面向對象的方式。例如,咱們在這裏構建了一個視圖控制器,用於編輯註釋--它接受兩個函數,一個用於加載它正在編輯的註釋的當前版本,另外一個用於嚮應用程序的中央數據存儲區提交更新:

class NoteEditorViewController: UIViewController {
    private let provideNote: Function<Void, Note>
    private let updateNote: Function<Note, Void>

    init(provideNote: Function<Void, Note>,
         updateNote: Function<Note, Void>) {
        self.provideNote = provideNote
        self.updateNote = updateNote
        super.init(nibName: nil, bundle: nil)
    }
    
    ...

    private func editorTextDidChange(to text: String) {
        var note = provideNote()
        note.text = text
        updateNote(note)
    }
}
複製代碼

上述方法的優勢在於,它容許咱們以一種與咱們用來驅動模型和數據邏輯的具體類型徹底解耦的方式構建UI。例如,上面的視圖控制器實際使用的函數在本例中是在NoteManager類型,看起來是這樣的:

class NoteManager {
    ...

    func loadNote(withID id: Note.ID) -> Note {
        ...
    }
    
    func updateNote(_ note: Note) {
        ...
    }
}
複製代碼

而後,當咱們建立視圖控制器的實例時,咱們使用的是Function將上述兩個方法轉換爲UI代碼能夠直接調用的函數,而沒必要知道任何底層類型或詳細信息:

func makeEditorViewController(
    forNoteID noteID: Note.ID
) -> UIViewController {
    let provider = Function(noteManager.loadNote).combined(with: noteID)
    let updater = Function(noteManager.updateNote)

    return NoteEditorViewController(
        provideNote: provider,
        updateNote: updater
    )
}
複製代碼

上述方法不只使咱們更好地分離了關注點,並且使測試變得垂手可得,由於咱們再也不須要模擬任何協議或與基於單例的全局狀態進行鬥爭,咱們能夠簡單地注入咱們但願測試的任何類型的行爲。傳入特定於測試的函數.

將密鑰路徑做爲函數傳遞

SWIFT 5.2中引入的另外一個很是有趣的新特性是關鍵路徑如今能夠做爲函數傳遞。當咱們使用閉包從屬性中提取數據時,這很是方便--由於咱們如今能夠直接傳遞該屬性的關鍵路徑:

let notes: [Note] = ...

// Before:
let titles = notes.map { $0.title }

// After:
let titles = notes.map(\.title)
複製代碼

將這種能力與咱們的Function從之前的類型開始,咱們如今能夠輕鬆地構造一個函數鏈,它容許咱們加載一個給定的值,而後從它中提取一個屬性。在這裏,咱們只是建立一個函數,使咱們可以輕鬆地查找與給定的便箋ID相關聯的標記:

func tagLoader(forNoteID noteID: Note.ID) -> Function<Void, [Tag]> {
    Function(noteManager.loadNote)
        .combined(with: noteID)
        .chained(to: \.tags)
}
複製代碼

固然,當咱們開始將函數式編程模式和麪向對象的API相結合時,上面的例子幾乎沒有觸及到什麼是可能的--因此這絕對是咱們在之後的文章中要討論的話題。

結語

SWIFT 5.2和Xcode 11.4都是至關重要的版本--有一個新的編譯器錯誤診斷引擎、許多新的測試和調試特性,以及更多。但從語法角度看,SWIFT5.2也是一個有趣的版本,由於它繼續拓寬SWIFT能夠用來採用函數式編程概念的方式,以及它如何開始模糊類型和函數之間的界限。

相關文章
相關標籤/搜索