这里每天分享一个 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)
}
}
它接受两个参数:function
和 around
,并且返回一个新的函数,这个新函数也接受与原函数相同的参数,并将它们传递给 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新知",每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!