探讨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能够用来采用函数式编程概念的方式,以及它如何开始模糊类型和函数之间的界限。

相关文章
相关标签/搜索