详细讲讲 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新知",每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

相关推荐
开心就好202521 小时前
iOS App 安全加固流程记录,代码、资源与安装包保护
后端·ios
开心就好202521 小时前
iOS App 性能测试工具怎么选?使用克魔助手(Keymob)结合 Instruments 完成
后端·ios
zhongjiahao2 天前
面试常问的 RunLoop,到底在Loop什么?
ios
wvy3 天前
iOS 26手势返回到根页面时TabBar的动效问题
ios
RickeyBoy3 天前
iOS 图片取色完全指南:从像素格式到工程实践
ios
aiopencode4 天前
使用 Ipa Guard 命令行版本将 IPA 混淆接入自动化流程
后端·ios
二流小码农4 天前
鸿蒙开发:路由组件升级,支持页面一键创建
android·ios·harmonyos
iceiceiceice5 天前
iOS PDF阅读器段评实现:如何从 PDFSelection 精准还原一个自然段
前端·人工智能·ios
TT_Close5 天前
【Flutter×鸿蒙】FVM 不认鸿蒙 SDK?4步手动塞进去
flutter·swift·harmonyos
张江5 天前
Swift Concurrency学习
swift