Swift 闭包支持隐式 self

2022-11-09 21:20:29 浏览数 (2)

Swift 5.8 实现: SE-0365

相关介绍

在 closure 捕获列表中,如果显式捕获self,则在 closure 使用时,则允许隐式使用self。即在 closure 捕获列表中声明[self], 则 closure 内调用self.的地方都可以不用书写该self.。这个特性在SE-0269中提议。现在本篇提议想把这个特性支持扩展到weak self的捕获上,并允许隐式self和已解包的self一样使用。效果就是如果在 closure 内 self 已经解包,则 closure 内调用 self 的地方可以不用写 self。如下例的dismiss方法调用:

代码语言:Swift复制
class ViewController {
    let button: Button

    func setup() {
        button.tapHandler = { [weak self] in
            guard let self else { return }
            dismiss()
        }
    }

    func dismiss() { ... }
}

以前在 closure 中都是需要显式指定self,目的是为了防止开发者无意之中产生循环引用。SE-0269其实放宽了这个规则,因为隐式self不太可能引入潜在隐藏的循环引用(这里只是说可能性小一点,如果是嵌套使用,这个场景是容易出现的)。例如当self在闭包的捕获列表中被显式捕获时:

代码语言:Swift复制
button.tapHandler = { [self] in
    dismiss()
}

SE-0269 其实没有实现对weak self捕获的处理。所以tapHandler如果捕获weak self,则在 closure 中调用dismiss,则需要显式使用self

代码语言:Swift复制
button.tapHandler = { [weak self] in
    guard let self else { return }
    self.dismiss()
}

上例的处理是当前针对weak self的普遍处理操作。其实可以看到self已经在 closure 中被显式捕获,后面再显式要求使用self显得没那么必要,相反,此时不使用self,反而更加清晰。这是本篇提议想要解决的问题。

提议解决方案

只要self已经解包,我们提议允许隐式self作为weak self的捕获,去掉self.的显式写法。

利用上述改进,下面 closure 中的dismiss()方法可以不用self.也可以编译通过:

代码语言:Swift复制
class ViewController {
    let button: Button

    func setup() {
        button.tapHandler = { [weak self] in
            guard let self else { return }
            dismiss()
        }
    }

    func dismiss() { ... }
}

详细设计

启用隐式 self

下面列举 6 种形式的可选解绑,全部支持这些情况,并在解包后的所有作用域范围内,都支持隐式self。下面列表全部的可选解绑类型:

代码语言:Swift复制
button.tapHandler = { [weak self] in
  guard let self else { return }
  dismiss()
}

button.tapHandler = { [weak self] in
  guard let self = self else { return }
  dismiss()
}

button.tapHandler = { [weak self] in
  if let self {
    dismiss()
  }
}

button.tapHandler = { [weak self] in
  if let self = self {
    dismiss()
  }
}

button.tapHandler = { [weak self] in
  while let self {
    dismiss()
  }
}

button.tapHandler = { [weak self] in
  while let self = self {
    dismiss()
  }
}

同样跟strongunowned的隐式self捕获一样,编译器会自动合成一个self.。对于调用在某个 closure 里面调用self的属性或者方法,则要用weak self

那么如果对weak self没有解包而直接使用隐式调用用法,会发生什么?此时编译器会直接报错,告诉这种情况需要显式指定self?., 才能清晰地表达整个控制流。

代码语言:Swift复制
button.tapHandler = { [weak self] in
    // error: explicit use of 'self' is required when 'self' is optional,
    // to make control flow explicit
    // fix-it: reference 'self?.' explicitly
    dismiss()
}

嵌套 closure

嵌套 closure 是产生循环引用的源头,需要谨慎处理。举个例子,下面伪代码couldCauseRetainCycle如果可以正常编译,那么隐式的self.bar()调用将会引入一个隐藏的循环引用。

代码语言:Swift复制
couldCauseRetainCycle { [weak self] in
  guard let self else { return }
  foo()

  couldCauseRetainCycle {
    bar()
  }
}

调用到foo()都没有问题,但是 closure 继续调用couldCauseRetainCycle, 而且其中bar()在作用域捕获的当前的self,而不是 guard 处解包的self,结果一个隐藏的循环引用就产生了。所以对于嵌套的 closure, 为了使用隐式的self,必须显式捕获self。下面 3 种情况都可以,看下改进的代码示例:

代码语言:Swift复制
// Not allowed:
couldCauseRetainCycle { [weak self] in
  guard let self else { return }
  foo()

  couldCauseRetainCycle {
    // error: call to method 'method' in closure requires 
    // explicit use of 'self' to make capture semantics explicit
    bar()
  }
}

// Allowed:
couldCauseRetainCycle { [weak self] in
  guard let self else { return }
  foo()

  couldCauseRetainCycle { [weak self] in
    guard let self else { return }
    bar()
  }
}

// Also allowed:
couldCauseRetainCycle { [weak self] in
  guard let self else { return }
  foo()

  couldCauseRetainCycle {
    self.bar() // 上面的 self, 已经 weak
  }
}

// Also allowed:
couldCauseRetainCycle { [weak self] in
  guard let self else { return }
  foo()

  couldCauseRetainCycle { [self] in
    bar()
  }
}

总结

针对 weak self 在 closure 中的捕获,在解包的前提下,我们可以在 closure 的作用域内使用隐式self,来代替当前使用的显式self

0 人点赞