详细讲讲 swift 5.9 出的新语法:参数包

这里每天分享一个 iOS 的新知识,快来关注我吧

前言

Swift 5.9 出了一个新功能,Swift Parameter Packs(Swift 参数包),在之前的文章中也有提到过,不过当时没讲参数包应该如何使用,今天就来学习一下什么是参数包,以及在日常开发中有哪些应用场景。

升级 Xcode 15 之后,你能用到哪些 swift 新特性

什么是 Swift Parameter Packs

我们用一个例子来说明,当我们在函数中使用泛型时,可能是这样的:

swift 复制代码
func eachFirst<T>(_ item: T) -> T?

当我们使用两个泛型时:

swift 复制代码
func eachFirst<T1, T2>(_ item1: T1, _ item2: T2) -> (T1?, T2?)

当我们使用三个泛型时:

swift 复制代码
func eachFirst<T1, T2, T3>(_ item1: T1, _ item2: T2, _ item3: T3) -> (T1?, T2?, T3?)

大家发现问题了吗?当使用的泛型越来越多,就会使得函数越来越长,越来越丑。

在 SwiftUI 框架中 buildBlock 函数竟然多达 10 个泛型:

swift 复制代码
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View, C8: View, C9: View

这看起来是不是非常令人头疼,swift 为了解决这个问题,提出了 Swift Parameter Packs 的概念,简单来讲就是,可以用 each 关键字来代替那些重复的代码,使用 each 之后的 buildBlock 函数变为:

swift 复制代码
static func buildBlock<each Content>(_ content: repeat each Content) -> TupleView<(repeat each Content)> where repeat each Content : View

对比一下是不是简洁多了。

接下来讲几个实用的例子。

KeyPath 取值

比如我有个用户类,然后我想要取其中的属性,并以元组的方式返回:

php 复制代码
class User {
    let id: String
    let name: String
    let age: Int
    init(id: String, name: String, age: Int) {
        self.id = id
        self.name = name
        self.age = age
    }
}

func valueAt(_ object: User, keyPath1: KeyPath<User, String>, keyPath2: KeyPath<User, String>, keyPath3: KeyPath<User, Int>) -> (String, String, Int) {
    return (object[keyPath: keyPath1], object[keyPath: keyPath2], object[keyPath: keyPath3])
}

let user = User(id: "0", name: "iOS 新知", age: 1)
print(valueAt(user, keyPath1: \.id, keyPath2: \.name, keyPath3: \.age))

实际上,这种写法既不优雅,也很麻烦,还不通用。

使用参数包的形式改一下:

swift 复制代码
func valuesAt<T, each U>(_ subject: T, keyPaths keyPath: repeat KeyPath<T, each U>) -> (repeat each U) {
    (repeat (subject[keyPath: each keyPath]))
}

修改完调用只需要:

scss 复制代码
valuesAt(user, keyPaths: \.id, \.name, \.age)

后面的参数可以传任意多个,而且使用泛型之后不仅 User 可以调用,其他的类也能使用。

缓存性能开销大的函数

有一些函数可能需要时间比较久,或者性能开销比较大,这时候的优化方案通常需要把操作过的缓存起来,不要每次都重复执行,比如加载图片比较耗时,那么就需要把加载过的图片缓存起来,那么你可能有这样的函数:

swift 复制代码
func fetchImage(url: String) -> UIImage {
    if let result = storage[url] {
        return result
    } else {
        // TODO: 加载图片
        return xxx
    }
}

这个方法不好的地方在于,它只适用于你当下图片的场景,可以把这一类需求使用参数包给封装起来:

sql 复制代码
func memoize<each Argument: Hashable, Return>(
    _ function: @escaping (repeat each Argument) -> Return
) -> (repeat each Argument) -> Return {
    var storage = [AnyHashable: Return]()
    
    return { (argument: repeat each Argument) in
        var key = [AnyHashable]()
        repeat key.append(AnyHashable(each argument))
        
        if let result = storage[key] {
            return result
        } else {
            let result = function(repeat each argument)
            storage[key] = result
            return result
        }
    }
}

这个函数要求传入一个函数,并返回一个新函数,那么上边缓存图片的例子就可以用这个函数实现:

scss 复制代码
// loadImage 是加载图片的函数
let memoizedLoadImage = memoize(loadImage)
memoizedLoadImage(URL(filePath: "some-url"))
memoizedLoadImage(URL(filePath: "some-url"))
memoizedLoadImage(URL(filePath: "other-url"))

当调用 memoizedLoadImage 函数时,函数体内会先从 storage 检查,如果存在直接返回,如果不存在,则调用传入的 loadImage 方法来获取。

装饰高阶函数

可以使用参数包把高阶函数封装起来,以便通用,举个例子:

less 复制代码
func decorateAround<each Argument, Return>(
    _ function: @escaping (repeat each Argument) -> Return,
    around: @escaping ((repeat each Argument) -> Return, repeat each Argument) -> Return
) -> (repeat each Argument) -> Return {
    { (argument: repeat each Argument) in
        around(function, repeat each argument)
    }
}

它接受两个参数:functionaround,并且返回一个新的函数,这个新函数也接受与原函数相同的参数,并将它们传递给 around 函数。相当于是把 function 装饰了一下。

听起来比较绕对吧?来看个例子:

swift 复制代码
func addTwoNumbers(_ a: Int, _ b: Int) -> Int {
    return a + b
}

// 创建一个装饰函数,用于在调用原始函数之前和之后输出信息
let decoratedAdd = decorateAround(addTwoNumbers) { (originalFunction, a, b) in
    print("调用 addTwoNumbers 之前")
    let result = originalFunction(a, b)
    print("调 addTwoNumbers 之后")
    return result
}

decoratedAdd(1, 2) // 打印 3

decorateAround 函数用于创建一个装饰函数 decoratedAdd,我在该函数在调用 addTwoNumbers 的前后输出两段信息。最后,我调用了装饰后的函数,观察输出的信息。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 "iOS新知",每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

相关推荐
大熊猫侯佩38 分钟前
SwiftUI 集合视图(Grid)拖放交换 Cell 的极简实现
swiftui·swift·apple
大熊猫侯佩1 小时前
Swift 结构化并发之全局 Actor 趣谈
swift·apple
2501_915106322 小时前
数据差异的iOS性能调试:设备日志导出和iOS文件管理
websocket·http·macos·ios·https·udp·cocoa
cxks-从新开始3 小时前
在 Mac 上配置 Charles,抓取 iOS 手机端接口请求
macos·ios·智能手机
云深不知处_18 小时前
RE0_OC_1
前端·ios
leluckys18 小时前
swift-协程
开发语言·ios·swift
杂雾无尘1 天前
iOS 分享扩展(四):让分享扩展与主应用无缝衔接
ios·swift·apple
烈焰晴天1 天前
新发布的一款使用ReactNative新架构加载Svga动画的开源插件[android/ios]
android·react native·ios
前端与小赵1 天前
ios如何把H5网页变成主屏幕webapp应用
ios·web app