【问题标题】:Escaping Closures in Swift在 Swift 中转义闭包
【发布时间】:2017-01-23 01:42:06
【问题描述】:

我是 Swift 新手,当我遇到转义闭包时,我正在阅读手册。我根本没有得到手册的描述。有人可以简单地向我解释一下 Swift 中的转义闭包是什么。

【问题讨论】:

  • 从手册中引用,“当闭包作为参数传递给函数时,闭包被称为转义函数,但在函数返回后被调用。”所以,如果闭包是同步调用的,它是非转义的。一个示例可能是枚举闭包,或mapfilter 等函数方法。如果它被异步调用(即稍后),它正在转义。转义闭包的最常见示例是一些慢速异步任务的完成处理程序,例如网络请求。
  • 如果您认为我的回答回答了您的问题,请考虑点击该复选标记接受。

标签: swift closures


【解决方案1】:

考虑这个类:

class A {
    var closure: (() -> Void)?
    func someMethod(closure: @escaping () -> Void) {
        self.closure = closure
    }
}

someMethod 将传入的闭包分配给类中的一个属性。

现在又来了一个类:

class B {
    var number = 0
    var a: A = A()
    func anotherMethod() {
        a.someMethod { self.number = 10 }
    }
}

如果我调用anotherMethod,闭包{ self.number = 10 } 将存储在A 的实例中。由于self 在闭包中被捕获,A 的实例也将持有对它的强引用。

这基本上是一个逃逸闭包的例子!

您可能想知道,“什么?那么闭包是从哪里逃出来的?”

闭包从方法的范围逃逸到类的范围。它可以在以后调用,甚至在另一个线程上!如果处理不当,这可能会导致问题。

默认情况下,Swift 不允许闭包逃逸。您必须将 @escaping 添加到闭包类型中以告诉编译器“请允许此闭包转义”。如果我们删除@escaping:

class A {
    var closure: (() -> Void)?
    func someMethod(closure: () -> Void) {
    }
}

并尝试写self.closure = closure,它不会编译!

【讨论】:

  • noescape 是 Swift 3 中的默认行为,已弃用。您现在需要做的是使用转义。 func someMethod(closure: @escaping () -> Void)
  • @bandejapaisa 感谢您提供信息!答案已编辑!
  • @bandejapaisa 您说“@noescape”已被弃用,但我最近遇到了一种情况,让我认为它只是半弃用。请看看这个,如果我犯了一些错误,请告诉我:stackoverflow.com/questions/41917413/…
  • @noescape 是隐式行为。该关键字已被弃用,尚未完全删除,但您通过编写或不编写它得到的行为是您的闭包不会脱离方法的范围。不推荐使用意味着它仍然可以使用,但它将在未来的版本中被删除。因此,为什么您仍然可以使用它,以及为什么您认为它是“半弃用”
  • 从您的回答中,我仍然不明白其中的区别——每种类型有什么用处。如果你提供一个真实的例子会更好。其他两个答案更容易理解。
【解决方案2】:

I find this website very helpful on that matter 简单的解释是:

如果闭包作为参数传递给函数并被调用 函数返回后,闭包正在转义。

在我上面传递的链接上阅读更多内容! :)

【讨论】:

    【解决方案3】:

    我将采用更简单的方式。

    考虑这个例子:

    func testFunctionWithNonescapingClosure(closure:() -> Void) {
            closure()
    }
    

    上面是一个非转义闭包,因为闭包是 在方法返回之前调用。

    考虑使用异步操作的相同示例:

    func testFunctionWithEscapingClosure(closure:@escaping () -> Void) {
          DispatchQueue.main.async {
               closure()
          }
     }
    

    上面的例子包含了一个转义的闭包,因为闭包调用可能发生在函数返回之后,因为异步操作。

     var completionHandlers: [() -> Void] = []
     func testFunctionWithEscapingClosure(closure: @escaping () -> Void) {
          completionHandlers.append(closure)
     }
    

    在上述情况下,您可以很容易地意识到闭包正在向外移动 函数体,所以它需要是一个转义闭包。

    在 Swift 3 中为编译器优化添加了转义和非转义闭包。您可以搜索nonescaping 闭包的优点。

    【讨论】:

    • 最好在这个好答案中添加非转义闭包的优点以及何时需要它的示例。
    【解决方案4】:

    斯威夫特 4.1

    来自语言参考:Attributes of The Swift Programming Language (Swift 4.1)

    苹果清楚地解释了escaping这个属性。

    将此属性应用于方法或函数声明中的参数类型,以指示可以存储参数的值以供以后执行。这意味着允许该值超过调用的生命周期。带有转义类型属性的函数类型参数需要显式使用 self。用于属性或方法。有关如何使用转义属性的示例,请参阅Escaping Closures

    var completionHandlers: [() -> Void] = []
    func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
        completionHandlers.append(completionHandler)
    }
    

    someFunctionWithEscapingClosure(_:) 函数将闭包作为其参数,并将其添加到在函数外部声明的数组中。如果你没有用@escaping标记这个函数的参数,你会得到一个编译时错误。

    当闭包作为参数传递给函数时,闭包被称为转义函数,但在函数返回后被调用。当您声明一个将闭包作为其参数之一的函数时,您可以在参数类型前写@escaping 以指示允许闭包转义。

    【讨论】:

      【解决方案5】:

      默认情况下,闭包是非转义的。为了简单理解,您可以将 non_escaping 闭包视为局部闭包(就像局部变量一样),将转义视为全局闭包(就像全局变量一样)。这意味着一旦我们从方法体中出来,non_escaping 闭包的范围就会丢失。但是在转义闭包的情况下,内存将闭包保留在内存中。

      ***当我们在方法中的任何异步任务中调用闭包时,或者在调用闭包之前方法返回时,我们只是使用转义闭包。

      非转义闭包:-

      func add(num1: Int, num2: Int, completion: ((Int) -> (Void))) -> Int {
          DispatchQueue.global(qos: .background).async {
              print("Background")
              completion(num1 + num2) // Error: Closure use of non-escaping parameter 'completion' may allow it to escape
          }
          return num1
      }
      
      override func viewDidLoad() {
          super.viewDidLoad()
          let ans = add(num1: 12, num2: 22, completion: { (number) in
              print("Inside Closure")
              print(number)
          })
          print("Ans = \(ans)")
          initialSetup()
      }
      

      由于它是 non_escaping 闭包,一旦我们从 'add' 方法中出来,它的作用域就会丢失。 completion(num1 + num2) 永远不会调用。

      转义闭包:-

      func add(num1: Int, num2: Int, completion: @escaping((Int) -> (Void))) -> Int {
          DispatchQueue.global(qos: .background).async {
              print("Background")
              completion(num1 + num2)
          }
          return num1
      }
      

      即使方法返回(即我们离开方法范围),闭包也会被调用。enter code here

      【讨论】:

        【解决方案6】:

        非转义(@noescape)与转义(@escaping)闭包

        [Function and closure]

        non-escaping closure

        @noescape 是一个闭包,它被传递给一个函数,并在函数返回之前调用

        non-escaping closure 的一个很好的例子是 Array sort function - sorted(by: (Element, Element) -> Bool)。在执行排序计算期间会调用此闭包。

        历史记录:@noescapeSwift 2引入 -> 在 Swift 3弃用 成为 默认 这就是原因您应该明确标记@escaping 属性。

        func foo(_ nonEscapingClosure: () -> Void) {
            nonEscapingClosure()
        }
        

        escaping closure

        方法结束时转义闭包(引用)仍然有效。

        //If you have next error
        Escaping closure captures non-escaping parameter
        //`@escaping` to the rescue 
        

        @escaping 是一个闭包

        1. 传入函数
        2. 所有者函数将此闭包保存在属性中
        3. 闭包被调用(使用属性)所有者函数返回(异步)

        escaping closure 的一个很好的例子是异步操作中的completion handler。如果你没有用@escaping 标记你的函数,在这种情况下你会得到编译时错误。在 escaping closure 中引用属性要求您明确使用 self

        class MyClass {
            var completionHandler: (() -> Void)?
        
            func foo(_ escapingClosure: @escaping () -> Void) {
                
                //if you don't mark as @escaping you get
                //Assigning non-escaping parameter 'escapingClosure' to an @escaping closure
                completionHandler = escapingClosure //<- error here
            }
        
            func onEvent() {
                completionHandler?()
            }
        }
        

        [Sync vs Async]

        【讨论】:

          【解决方案7】:

          逃逸的定义

          Swift 的闭包是引用类型,这意味着如果您将两个变量指向同一个闭包,它们共享该闭包 - Swift 只记得通过增加其引用计数来依赖它的两件事。

          当一个闭包被传递给一个要使用的函数时,Swift 需要知道该函数是立即被使用还是被保存以供以后使用。如果立即使用它,编译器可以跳过将其引用计数加一,因为闭包将立即运行然后被遗忘。但如果它稍后使用——或者甚至可能在以后使用——Swift 需要在它的引用计数上加一,这样它就不会被意外销毁。

          快速示例

          转义闭包的一个很好的例子是完成处理程序。它在未来执行,当一个冗长的任务完成时,它比创建它的函数的寿命更长。另一个例子是异步编程:异步执行的闭包总是转义其原始上下文。

          public func responseData(
              queue: DispatchQueue? = nil,
              completionHandler: @escaping (DataResponse<Data>) -> Void)
              -> Self
          {
              ...
          

          额外信息

          出于性能原因,Swift 假定所有闭包都是非转义闭包,这意味着它们将立即在函数内部使用而不是存储,这反过来意味着 Swift 不会触及引用计数。如果不是这种情况——如果你采取任何措施来存储闭包——那么 Swift 会强制你将其标记为 @escaping 以便必须更改引用计数。

          【讨论】:

            猜你喜欢
            • 2021-12-20
            • 2018-05-05
            • 1970-01-01
            • 1970-01-01
            • 2017-01-29
            • 2020-05-04
            • 1970-01-01
            • 1970-01-01
            • 2022-08-21
            相关资源
            最近更新 更多