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的一个强大元素,可以为特定情况带来更好的性能。了解它的含义以决定惰性数组是否适合您的场景是非常重要的。