Swift 6.0 中的参数包迭代

Swift 5.9 中引入的参数包使得编写可以抽象参数数量的泛型成为可能。这消除了为一个参数、两个参数、三个参数等等的相同泛型函数提供重载副本的需要。在 Swift 6.0 中,包迭代使得使用参数包比以往任何时候都更容易。这篇文章将向您展示如何最好地利用包迭代。

参数包回顾

首先,让我们回顾一下参数包。考虑以下代码

let areEqual = (1, true, "hello") == (1, false, "hello")
print(areEqual)
// false

上面的代码只是简单地比较两个元组。但是,如果元组包含 7 个元素,这段代码将无法工作!

长期以来,Swift 标准库仅为最多 6 个元素的元组提供了比较运算符

func == (lhs: (), rhs: ()) -> Bool

func == <A, B>(lhs: (A, B), rhs: (A, B)) -> Bool where A: Equatable, B: Equatable

func == <A, B, C>(lhs: (A, B, C), rhs: (A, B, C)) -> Bool where A: Equatable, B: Equatable, C: Equatable

// and so on, up to 6-element tuples

在上面的每个泛型函数中,输入元组的每个元素都必须在函数的泛型参数列表中声明其类型。因此,每当我们想要支持更大的元组大小时,都需要向泛型参数列表添加新元素。因此,人为地限制了 6 元素元组。

参数包添加了在可变数量的类型参数上抽象函数的能力。这意味着我们可以使用像这样编写的 == 运算符来解除 6 元素限制

func == <each Element: Equatable>(lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool

让我们分解一下在上面的签名中看到的类型

使用参数包实现的元组相等运算符,让我们再次查看调用站点,以便更好地理解这些概念。

let areEqual = (1, true, "hello") == (1, false, "hello")
print(areEqual)
// false

== 的调用将类型包 {Int, Bool, String} 替换为 Element 类型包。请注意,lhsrhs 都具有相同的类型。最后,使用值包 {1, true, "hello"} 调用函数 ==,用于 lhs 元组的值包,以及 {1, false, "hello"} 用于 rhs 元组的值包。

为何需要包迭代?

新的元组比较运算符签名的示例看起来很棒,但是我们如何在函数体内部实际使用 lhsrhs 元组的值呢?请随意花点时间思考一下。

事实证明,在 Swift 6.0 之前,根本没有简洁的方法来实现该函数。一种解决方案是创建一个本地函数,该函数比较来自两个元组的一对元素,然后使用包扩展为每对元素调用该函数,如下所示

struct NotEqual: Error {}

func == <each Element: Equatable>(lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool {
  // Local throwing function for operating over each element of a pack expansion.
  func isEqual<T: Equatable>(_ left: T, _ right: T) throws {
    if left == right {
      return
    }
    
    throw NotEqual()
  }

  // Do-catch statement for returning false as soon as two tuple elements are not equal.
  do {
    repeat try isEqual(each lhs, each rhs)
  } catch {
    return false
  }

  return true
}

上面的代码看起来不太好,对吧?为了简单地检查每对元素的条件,我们需要声明一个本地函数 isEqual,它只是比较给定的元素。但是,这不足以使函数提前返回,因为在扩展参数包 lhsrhs 时,仍然会在每对元素上调用本地 isEqual 函数。因此,isEqual 必须标记为 throws,并在找到一对不匹配的元素时抛出错误。然后,我们在 catch 块中捕获错误以返回 false

引入包迭代

Swift 6.0 通过引入使用熟悉的 for-in 循环语法的包迭代,大大简化了这项任务。

更具体地说,通过包迭代,== 元组比较运算符的主体简化为一个简单的 for-in repeat 循环

func == <each Element: Equatable>(lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool {

  for (left, right) in repeat (each lhs, each rhs) {
    guard left == right else { return false }
  }
  return true
}

在上面的代码中,我们能够利用 for-in 循环功能来成对迭代元组。

请注意,在迭代包时,我们使用新的 for-in repeat 语法,后跟我们要迭代的值参数包。在每次迭代中,循环将值参数包的每个元素绑定到局部变量。这意味着在本例中,lhs 的第 i 个元素将在第 i 次迭代中绑定到局部变量 left。在循环体中,您可以像往常一样使用局部变量。在我们的例子中,我们比较每对元素,并在找到 left != right 的一对时返回 false,使用熟悉的 guard 语句。当然,我们不再需要像以前那样抛出任何错误!

使用包迭代

现在,让我们通过一些示例来探索更多在 Swift 代码中使用包迭代的方法。

首先,考虑这样一种情况:您需要编写一个函数来检查给定值参数包中的所有数组是否为空

func allEmpty<each T>(_ array: repeat [each T]) -> Bool {
  for a in repeat each array {
    guard a.isEmpty else { return false }
  }

  return true
}

上面的函数是关于类型参数包 each T 的泛型函数,并接受值参数包 array,其类型使用 repeat [each T] 包扩展声明,其中 [each T] 是重复模式。在调用站点,它为替换包中的每个元素重复,从而使值扩展为数组字面量列表。

for-in repeat 循环的每次迭代中,值参数包 array 的元素都绑定到局部变量 a。请注意,通过包迭代,值包的元素按需评估,这意味着我们可以在不检查值包的所有数组的情况下提前从函数返回。在本例中,我们使用了 guard 语句。

以下是如何使用 allEmpty 函数的方法

print(allEmpty(["One", "Two"], [1], [true, false], []))
// False

现在,让我们看一个参数包高级用法的示例,包迭代大大简化了这种用法。首先,让我们声明以下协议

protocol ValueProducer {
  associatedtype Value: Codable
  func evaluate() -> Value
}

上面的协议 ValueProducer 需要 evaluate() 方法,该方法的返回类型是关联类型 Value,它符合 Codable 协议。

假设您获得类型为 Result<ValueProducer, Error> 的值参数包,并且您只需要迭代 success 元素,并在其值上调用 evaluate() 方法。此外,假设您需要将每次调用的结果保存到数组中。包迭代使这项任务变得非常容易!

func evaluateAll<each V: ValueProducer, each E: Error>(result: repeat Result<each V, each E>) -> [any Codable] {
  var evaluated: [any Codable] = []
  for case .success(let valueProducer) in repeat each result {
    evaluated.append(valueProducer.evaluate())
  }

  return evaluated
}

首先让我们注意 evaluateAll 函数的签名。在泛型参数列表中,它声明了两个类型参数包:each V: ValueProducereach E: Erroreach V 包的每个元素都必须符合上面声明的协议 ValueProducer,并且 each E 包的每个元素都必须符合 Error 协议。该函数接受一个参数 result,其包扩展类型为 repeat Result<each V, each E>。这意味着模式 Result<each V, each E> 将为 each Veach E 包的每个元素在运行时重复。

为了实现函数体,我们首先初始化 evaluated 数组。接下来,请注意我们如何使用 for case 模式仅针对 Result 枚举的 success 情况执行循环体。我们可以获取 valueProducer 变量,它将包含 ValueProducer 类型的值。现在,我们可以将调用 evaluate() 方法的结果附加到我们的 evaluated 数组中,我们最终返回该数组。

以下是如何使用此函数的方法

struct IntProducer: ValueProducer {

  let contained: Int

  init(_ contained: Int) {
    self.contained = contained
  }

  func evaluate() -> Int {
    return self.contained
  }
}

struct BoolProducer: ValueProducer {

  let contained: Bool

  init(_ contained: Bool) {
    self.contained = contained
  }

  func evaluate() -> Bool {
    return self.contained
  }
}

struct SomeError: Error {}

print(evaluateAll(result:
                    Result<SomeValueProducer, SomeError>.success(SomeValueProducer(5)),
                    Result<SomeValueProducer, SomeError>.failure(SomeError()),
                    Result<BoolProducer, SomeError>.success(BoolProducer(true))))

// [5, true]

总结

我们很高兴将包迭代引入 Swift 6.0!正如本文中所见,包迭代使与值参数包的交互变得更加直接,从而使这种高级功能更易于访问和直观地集成到您的 Swift 代码中。