探讨SWIFT 5.2的新功能特性

2,685 阅读8分钟

从表面上看,SWIFT 5.2在新的语言特性方面肯定是一个小版本,因为这个新版本的大部分重点是提高SWIFT底层基础结构的速度和稳定性,例如如何报告编译器错误,以及如何解决构建级依赖。

然而,斯威夫特5.2总数新的语言特性可能相对较小,它确实包括两个新功能,它们可能会对SWIFT的整体功能产生相当大的影响。函数式程序设计语言.

本周,让我们探讨这些特性,以及我们如何可能使用它们来接受一些在函数式编程世界中非常流行的不同范例--在面向对象的SWIFT代码库中,它们可能会感觉更加一致和熟悉。

在我们开始之前,作为Xcode 11.4的一部分,SWIFT5.2仍然处于测试版,请注意,本文是一篇非常探索性的文章,代表了我对这些新语言特性的第一印象。随着我在生产中使用新特性获得更多经验,我的观点可能会发生变化,尽管我将尝试在这种情况下更新这篇文章,但我建议您使用本文作为灵感,亲自探索这些新特性,而不是直接使用以原样呈现的解决方案。

有了这个小小的免责声明,让我们开始探索吧!

作为一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:1012951431 不管你是小白还是大牛欢迎入驻 ,分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!

另附上一份各好友收集的大厂面试题,进群可自行下载!

调用类型为函数

尽管SWIFT并不是一种严格的函数式编程语言,但毫无疑问,函数在其总体设计和使用中扮演着非常重要的角色。从闭包如何作为异步回调使用,到集合如何大量使用典型的函数模式(如map和reduce-职能无处不在。

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