避免在 DispatchQueue 中使用 self

Avoiding using self in the DispatchQueue

我想知道如何消除在 DispatchQueue 中使用 self。作为一种好的做法,我们应该只在 init()

中使用 self
func loadAllClasses() {
    DispatchQueue.global(qos: .background).async {
      self.classVM.fetchAllClasses(id: id, completion: { (classes, error) in
        DispatchQueue.main.async {
          if error != nil {
            self.showAlert(message: "try again", title: "Error")
          }
          if let classes = classes {
            self.classesList = classes
            self.classesCollectionView.reloadData()
          }
        }
      })
    }
  }

别担心! DispatchQueue 闭包不会导致保留循环。

“作为一种好的做法,我们应该只在 init() 中使用 self”

这当然不是真的。事实上,您使用 self 的次数比您在代码中看到的要多,因为这是编译器在您使用成员变量或成员函数时设置的隐式参数。基本上,您将无法按照此规则编写任何“真正的”面向对象代码。

此外,在 init 函数中使用 self 甚至 特殊 - 因为 self 是对“ under construction”,甚至在值还没有完全初始化的情况下,甚至不允许使用self

所以,话虽如此,在将要分派的闭包中使用 self 是完全不同的事情。

强攻self:

给定一个在闭包中使用 self 的成员函数,如下所示:

func loadAllClasses() {
    queue.async {
        self.foo = 
    }
}

在这里,您捕获了对 self 的强引用,这将使 self 引用的对象至少在闭包被调用和完成之前保持活动状态。

我们可以将其称为“临时”保留周期 - 但假设完成处理程序将被调用 最终 闭包将被释放,并且强引用将被释放 self 还有。

所以,这会自动解决 - 我们不必担心......,除了我们犯了一些程序员错误,其中不会调用完成处理程序(不过这是另一章;))

弱捕获self:

如果需要,您可以阻止闭包使对象保持活动状态 - 通过弱捕获 self 。这是必须做出的决定,在某些情况下你可能希望让对象保持活动状态,而在其他情况下你不希望对象在关闭完成之前一直处于活动状态.

func loadAllClasses() { 
    queue.async { [weak self] in 
        self?.foo = 
    }
}

这里,闭包只有对 self 的弱引用,因此,它不会让对象保持活动状态,直到它被调用为止。在闭包中,您需要获得对 weak self 的临时强引用才能使用它。注意闭包执行的时候可能是nil

使用一些其他技巧:

有时您需要 self 的 属性 中的特定值 - 但您不太关心 self 本身:

func loadAllClasses() { 
    let foo = self.foo
    queue.async { 
        let bar = foo
    }
}

在这里,你完全避免捕获self。你可以使用这个,当你可以说当你创建闭包时所有值都是已知的并且当你稍后在某个时间点执行闭包时它们的原始值的变化无关紧要。

你问过:

I wonder how to eliminate of using self inside the DispatchQueue.

在 Swift 5.3 中采用的 SE-0269 中,他们引入了一种模式,如果您在捕获列表中包含 self,则不需要所有 self 闭包中的引用:

func loadAllClasses() {
    DispatchQueue.global(qos: .background).async { [self] in  // note [self] capture list
        classVM.fetchAllClasses(id: id) { (classes, error) in
            DispatchQueue.main.async {
                if error != nil {
                    showAlert(message: "try again", title: "Error")
                }
                if let classes = classes {
                    classesList = classes
                    classesCollectionView.reloadData()
                }
            }
        }
    }
}

顺便说一句,fetchAllClasses 似乎已经是异步的,因此不需要向全局队列发送:

func loadAllClasses() {
    classVM.fetchAllClasses(id: id) { [self] (classes, error) in  // note [self] capture list
        DispatchQueue.main.async {
            if error != nil {
                showAlert(message: "try again", title: "Error")
            }
            if let classes = classes {
                classesList = classes
                classesCollectionView.reloadData()
            }
        }
    }
}

As a good practice, we are supposed to use self only in the init()?

不,只要可以消除歧义或需要明确潜在的强引用循环,就可以使用 self。不要回避使用 self 引用。

但是直觉,消除不必要的 self 引用是好的,因为在不需要的地方,它最终会成为句法噪音。如果您的更广泛的代码库到处都是 self 引用,那么在闭包中要求 self 引用的全部意义就被破坏了。


顺便说一句,上面我说明了你愿意捕获self的地方,你可以使用[self] in语法使自己的代码更简洁,消除很多不必要的self闭包内的引用。

话虽如此,在这种情况下我们通常希望使用 [weak self] 引用。当然,正如 vadian 所建议的那样,可能没有任何强大的参考循环风险。但这是所讨论对象的所需生命周期的问题。例如,让我们暂时假设 fetchAllClasses 可能非常慢。让我们进一步假设用户可能想要关闭有问题的视图控制器。

在那种情况下,您真的想让视图控制器保持活动状态以完成 fetchAllClasses,其唯一目的是更新已被关闭的集合视图吗?!?应该不是。

我们中的许多人在调用可能较慢的异步进程时会使用 [weak self]

func loadAllClasses() {
    classVM.fetchAllClasses(id: id) { [weak self] (classes, error) in  // note `[weak self]` so we don't keep strong reference to VC longer than we need
        DispatchQueue.main.async {
            guard let self = self else { return }                      // if view controller has been dismissed, no further action is needed

            guard error == nil, let classes = classes else {           // handle error and unwrap classes in one step
                self.showAlert(message: "try again", title: "Error")
                return
            }

            self.classesList = classes                                 // otherwise, proceed as normal
            self.classesCollectionView.reloadData()
        }
    }
}

是的,这重新引入了我们上面删除的 self 引用,但这是异步请求的一个很好的模式,一般来说:我们从不让一些旨在更新视图控制器的网络请求阻止该视图控制器不会在用户关闭时被释放。

上述模式的进一步改进是将 fetchAllClasses 设计为可取消的,然后在视图控制器的 deinit 中取消任何未决的网络请求(如果有)。这超出了这个问题的范围,但我们的想法是,我们不仅不应该保留视图控制器超过必要的时间,而且我们也应该取消挂起的请求。但是,这种 deinit 取消模式仅在您在闭包中使用 weak 引用时才有效。