iOS/Swift - 闭包/完成块和委托/函数有啥区别?

Posted

技术标签:

【中文标题】iOS/Swift - 闭包/完成块和委托/函数有啥区别?【英文标题】:iOS/Swift - What is the difference between Closure/Completion blocks and Delegates/functions?iOS/Swift - 闭包/完成块和委托/函数有什么区别? 【发布时间】:2019-07-09 06:51:16 【问题描述】:

我不清楚这两个,现在世界正在转向闭包类型。但我并没有清楚地理解这一点。有人可以用实时示例解释一下吗?

【问题讨论】:

【参考方案1】:

所以现实生活中的例子是这样的:

protocol TestDelegateClassDelegate: class 
    func iAmDone()


class TestDelegateClass 
    weak var delegate: TestDelegateClassDelegate?

    func doStuff() 
        DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) 
            self.delegate?.iAmDone()
        
    


class TestClosureClass 
    var completion: (() -> Void)?

    func doStuff() 
        DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) 
            self.completion?()
        
    



class ViewController: UIViewController, TestDelegateClassDelegate 

    func iAmDone() 
        print("TestDelegateClassDelegate is done")
    

    override func viewDidLoad() 
        super.viewDidLoad()

        let testingDelegate = TestDelegateClass()
        testingDelegate.delegate = self
        testingDelegate.doStuff()

        let testingClosure = TestClosureClass()
        testingClosure.completion = 
            print("TestClosureClass is done")
        
        testingClosure.doStuff()

    


这里我们有 2 个类 TestDelegateClassTestClosureClass。他们每个人都有一个方法 doStuff 等待 3 秒,然后向正在监听的人报告,其中一个使用委托过程,另一个使用关闭过程。

虽然他们除了等待什么都不做,你可以很容易地想象他们例如将图像上传到服务器并在完成时通知。因此,例如,您可能希望在上传过程中运行一个活动指示器,并在完成后停止它。它看起来像这样:

class ViewController: UIViewController, TestDelegateClassDelegate 

    @IBOutlet private var activityIndicator: UIActivityIndicatorView?

    func iAmDone() 
        print("TestDelegateClassDelegate is done")
        activityIndicator?.stopAnimating()
    

    override func viewDidLoad() 
        super.viewDidLoad()

        activityIndicator?.startAnimating()
        let testingDelegate = TestDelegateClass()
        testingDelegate.delegate = self
        testingDelegate.doStuff()

        activityIndicator?.startAnimating()
        let testingClosure = TestClosureClass()
        testingClosure.completion = 
            self.activityIndicator?.stopAnimating()
            print("TestClosureClass is done")
        
        testingClosure.doStuff()

    


当然,您只会使用这两个过程之一。

您可以看到代码存在巨大差异。要执行委托过程,您需要创建一个协议,在本例中为 TestDelegateClassDelegate。协议定义了侦听器的接口。并且由于定义了iAmDone 方法,它必须在ViewController 中定义,并且只要它被定义为TestDelegateClassDelegate。否则将无法编译。所以任何声明为TestDelegateClassDelegate 的东西都会有这个方法,任何类都可以调用它。在我们的例子中,我们有weak var delegate: TestDelegateClassDelegate?。这就是为什么我们可以调用delegate?.iAmDone() 而无需关心委托实际上是什么。例如我们可以创建另一个类:

class SomeClass: TestDelegateClassDelegate 
    func iAmDone() 
        print("Something cool happened")
    
    init() 
        let testingDelegate = TestDelegateClass()
        testingDelegate.delegate = self
        testingDelegate.doStuff()
    

所以一个很好的例子是 UITableView 使用 delegatedataSource (两者都是委托,只是属性名称不同)。并且表格视图将调用您为这些属性设置的任何类的方法,而无需知道该类是什么,只要它对应于给定的协议即可。

使用闭包也可以达到同样的效果。可以使用提供闭包的属性来定义表视图,例如:

tableView.onNumberOfRows  section in
    return 4

但这很可能会导致一大堆代码。在这种情况下,由于潜在的内存泄漏,闭包也会让许多程序员头疼。并不是说闭包不那么安全或其他什么,它们只是做了很多你看不到的代码,这可能会产生保留周期。在这种特定情况下,最有可能的泄漏是:

tableView.onNumberOfRows  section in
    return self.dataModel.count

而修复它只是简单地做

tableView.onNumberOfRows  [weak self] section in
    return self?.dataModel.count ?? 0

现在看起来过于复杂了。

我不会深入探讨闭包,但最终当您重复调用回调(例如在表格视图的情况下)时,您将需要在委托或闭包中使用 weak 链接。但是当闭包只被调用一次时(比如上传图片),闭包中不需要weak链接(在大多数但不是所有情况下)。

在回顾中尽可能多地使用闭包,但一旦闭包被用作属性(具有讽刺意味的是我给出的例子),就要避免或谨慎使用。但你宁愿这样做:

func doSomethingWithClosure(_ completion: @escaping (() -> Void)) 
    DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) 
        completion()
    

并将其用作

    doSomethingWithClosure 
        self.activityIndicator?.stopAnimating()
        print("TestClosureClass is done")
    

这已经消除了所有潜在的风险。我希望这能为您解决一两件事。

【讨论】:

【参考方案2】:

在 Swift/obj-c 中,术语 delegate 用于指代响应特定选择器的协议。

就像在对象上调用方法一样。

例如

protocol CalculatorDelegate : class  // : class so it can be made 'weak'
 func onCalculation(result: Int) -> Void

现在,如果我们有一个 Calculator 类,要使用委托,我们会做类似的事情

class Calculator() 
  weak var delegate: CalculatorDelegate?

  func calculate(_ a: Int, _ b: Int) -> Int 
    let result = a + b

    self.delegate?.onCalculation(result: result)
    return result
  

然后在其他一些类中(例如,在 ios 中 - 视图控制器)我们可能会这样做:

class MyClass : CalculatorDelegate 
 func onCalculation(result: Int) 
   print("Delegate method on calculation called with result \(result)")
 

 func someButtonPress() 
  let calculator = Calculator()
  calculator.delegate = self
  calculator.calculate(42, 66)
 

所以你可以看到设置是多么的复杂。

现在闭包只是可以在其他地方调用的代码块,因此您可以像这样更改所有代码:

class Calculator2() 
  weak var delegate: CalculatorDelegate?

  func calculate(_ a: Int, _ b: Int, onCalculation: (@escaping (Int) -> Void) -> Int)?) 
    let result = a + b

    onCalculation?(result)
    return result
  

class MyClass 
 func someButtonPress() 
  let calculator = Calculator2()
  calculator.calculate(42, 66, onCalculation:  (result: Int) in 
    print("Closure invoked with \(result)")
  )
 

但是,对于闭包,您需要注意,通过强烈捕获变量(例如 self)更容易使自己陷入困境,即使在 ARC 机制下也会导致内存泄漏。

【讨论】:

【参考方案3】:

闭包是一流的对象,因此它们可以嵌套和传递 简单地, 在 swift 中,函数是原始数据类型,如 int、double 或 character,这就是为什么您可以在函数参数中传递函数的原因。在 swift 机制中简化闭包语法,就像其他语言中的 lambda 表达式一样。

例如如果你想通过URSSession或Alamofire调用rest API并返回响应数据,那么你应该使用completionHandler(它是闭包)。

无效关闭:-(paramter:DataType)->Void

返回关闭:-(paramter:DataType)->DataType 例如(int, int) -> (int) https://docs.swift.org/swift-book/LanguageGuide/Closures.html

【讨论】:

以上是关于iOS/Swift - 闭包/完成块和委托/函数有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章

iOS Swift 如何访问在完成处理程序闭包中创建的数据——在闭包之外

从委托的源 ViewController 调用异步闭包

在 uiview 中有超过 1 个 uitextfield 的 iOS swift 委托

等待 iOS Swift CBPeripheralDelegate 完成的正确方法?

深入理解闭包中的委托

您是不是在具有闭包/委托/函数指针的编程语言中使用模板方法模式?