1. How and when to use Lazy Collections in Swift
惰性集合类似于常规集合,但改变了map、filter和reduce等修饰符的处理方式。根据我的经验,他们没有得到足够的关注,因为他们可以在某些情况下得到更好的表现。
您可能对惰性变量更为熟悉,但您以前在序列上使用过惰性属性吗?我将向您解释什么是惰性集合以及什么时候应该使用它们。
1.1. What is a lazy collection?
延迟收集将计算推迟到实际需要时。这在许多不同的情况下都是有益的,如果最终没有请求元素,就可以防止做不必要的工作。
下面的示例显示了一组数字,其中偶数是翻倍的。如果不使用lazy关键字,所有项都将在创建时直接处理:
var numbers: [Int] = [1, 2, 3, 6, 9]
let modifiedNumbers = numbers
.filter { number in
print("Even number filter")
return number % 2 == 0
}.map { number -> Int in
print("Doubling the number")
return number * 2
}
print(modifiedNumbers)
/*
Even number filter
Even number filter
Even number filter
Even number filter
Even number filter
Doubling the number
Doubling the number
[4, 12]
*/
正如您所看到的,两个偶数的翻倍发生在所有5个数字被过滤之后。
如果我们添加lazy关键字使数组计算修饰符为lazy,结果将会不同:
let modifiedLazyNumbers = numbers.lazy
.filter { number in
print("Lazy Even number filter")
return number % 2 == 0
}.map { number -> Int in
print("Lazy Doubling the number")
return number * 2
}
print(modifiedLazyNumbers)
// Prints:
// LazyMapSequence>, Int>(_base: Swift.LazyFilterSequence>(_base: [1, 2, 3, 6, 9], _predicate: (Function)), _transform: (Function))
事实上,修饰符根本没有被调用!这是因为我们还没有要这些号码。像filter和map这样的修改器只会在请求元素时执行:
print(modifiedLazyNumbers.first!)
/*
Prints:
Lazy Even number filter
Lazy Even number filter
Lazy Doubling the number
4
*/
你可以想象,如果只从一个大的收藏中使用一些物品,这可以为你节省大量的工作。
1.2. Handling output values on the go
惰性集合的另一个好处是可以随时处理输出值。例如,假设有一个头像图像获取器,您希望使用它来获取以字母A开头的用户名的头像。
如果没有lazy,它将执行如下:
let usernames = ["Antoine", "Maaike", "Jaap", "Amber", "Lady", "Angie"]
usernames
.filter { username in
print("filtered name")
return username.lowercased().first == "a"
}.forEach { username in
print("Fetch avatar for (username)")
}
/*
Prints:
filtered name
filtered name
filtered name
filtered name
filtered name
filtered name
Fetch avatar for Antoine
Fetch avatar for Amber
Fetch avatar for Angie
*/
首先过滤所有名字,然后获取所有以A开头的名字的化身。
虽然这样做是可行的,但是我们只能在整个集合被过滤之后才开始获取。如果我们必须遍历一个大的名称集合,这可能是一个缺点。
相反,如果我们在这个场景中使用惰性集合,我们将能够开始获取角色:
let usernames = ["Antoine", "Maaike", "Jaap", "Amber", "Lady", "Angie"]
usernames.lazy
.filter { username in
print("filtered name")
return username.lowercased().first == "a"
}.forEach { username in
print("Fetch avatar for (username)")
}
/*
Prints:
filtered name
Fetch avatar for Antoine
filtered name
filtered name
filtered name
Fetch avatar for Amber
filtered name
filtered name
Fetch avatar for Angie
*/
理解惰性数组和常规数组之间的区别很重要。一旦知道了什么时候执行修饰符,您就可以决定惰性集合对您的特定情况是否有意义。
1.3. Use opt-in over opt-out
现在您已经看到了惰性集合可以提高性能,您可能会想:“我将在任何地方都使用惰性集合!”但是,理解使用惰性数组的含义是很重要的。
1.3.1. Don’t over optimize
当使用lazy时,只有5个道具的集合不会给你带来很多性能上的胜利。这是一个因人而异的决定,它还取决于您的修改器所做的工作量。在大多数情况下,只有当您只打算使用大型集合中的少数项时,lazy才有用。
最重要的是,要知道惰性数组不会被缓存。
1.3.2. Lazy Collections don’t cache
惰性集合延迟执行修饰符,直到它们被请求。这也意味着结果值不会存储在输出数组中。事实上,所有的修饰符都会在每个条目请求上再次执行:
let modifiedLazyNumbers = numbers.lazy
.filter { number in
print("Lazy Even number filter")
return number % 2 == 0
}.map { number -> Int in
print("Lazy Doubling the number")
return number * 2
}
print(modifiedLazyNumbers.first!)
print(modifiedLazyNumbers.first!)
/*
Prints:
Lazy Even number filter
Lazy Even number filter
Lazy Doubling the number
4
Lazy Even number filter
Lazy Even number filter
Lazy Doubling the number
4
*/
而非惰性集合的相同场景只会计算一次输出值:
let modifiedNumbers = numbers
.filter { number in
print("Lazy Even number filter")
return number % 2 == 0
}.map { number -> Int in
print("Lazy Doubling the number")
return number * 2
}
print(modifiedNumbers.first!)
print(modifiedNumbers.first!)
/*
Prints:
Lazy Even number filter
Lazy Even number filter
Lazy Even number filter
Lazy Even number filter
Lazy Even number filter
Lazy Doubling the number
Lazy Doubling the number
4
4
*/
1.3.3. Take the delay into account
惰性集合只在请求时执行它的修饰符。如果其中一个修饰符执行的任务可能需要花费时间,您可能希望远离使用lazy。
换句话说,提前计算输出值并在实际需要时准备好它们可能是有益的。例如,您不希望在用户滚动时执行繁重的操作。
1.4. Consider using standard Swift APIs over lazy arrays
这本身就是一个主题,也是重新考虑使用惰性集合的另一个原因。Swift为我们提供了一整套优化的API来处理集合,这可能是一个更好的解决方案。
例如,你可能会认为在这个场景中使用lazy是一个明智的决定,因为它可以防止我们在只使用第一个元素之前过滤所有的数字:
let collectionOfNumbers = (1…1000000)
let lazyFirst = collectionOfNumbers.lazy
.filter {
print("filter")
return $0 % 2 == 0
}.first
print(lazyFirst) // Prints: 2
然而,在本例中,我们受益于使用first(where:)。这是一个标准的Swift API,它允许我们从所有的底层(未来)优化中受益:
let firstWhere = collectionOfNumbers.first(where: { $0 % 2 == 0 })
print(firstWhere) // Prints: 2
1.5. Conclusion
惰性集合是Swift的一个强大元素,可以为特定情况带来更好的性能。了解它的含义以决定惰性数组是否适合您的场景是非常重要的。