➽06闭包

Posted itzyjr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了➽06闭包相关的知识,希望对你有一定的参考价值。

闭包是自包含的功能块,可以在代码中传递和使用。Swift中的闭包类似于C和Objective-C中的块以及其他编程语言中的lambdas。
闭包可以从定义它们的上下文中捕获并存储对任何常量和变量的引用。这被称为关闭这些常量和变量。Swift为您处理捕获的所有内存管理。

闭包有三种形式之一:
● 全局函数是具有名称且不捕获任何值的闭包。
● 嵌套函数是具有名称的闭包,可以从其封闭函数中捕获值。
● 闭包表达式是以轻量级语法编写的未命名闭包,可以从其周围的上下文中捕获值。

Swift的闭包表达式有一个干净、清晰的风格,通过优化,在常见场景中鼓励简洁、无混乱的语法。这些优化包括:
● 从上下文推断参数和返回值类型
● 从单表达式闭包隐式返回
● 速记参数名称
● 跟踪闭包语法

闭包表达式
排序方法
Swift的标准库提供了一个名为sorted(by:)的方法,该方法根据您提供的排序闭包的输出对已知类型的值数组进行排序。完成排序过程后,sorted(by:)方法返回一个与旧数组类型和大小相同的新数组,其元素的排序顺序正确。原始数组不会被sorted(by:)方法修改。

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

sorted(by:)方法接受一个闭包,该闭包接受两个与数组内容类型相同的参数,并返回一个Bool值,以说明在对值进行排序后,第一个值应出现在第二个值之前还是之后。如果第一个值出现在第二个值之前,排序闭包需要返回true,否则返回false。

本例是对字符串值数组进行排序,因此排序闭包需要是(String, String)->Bool类型的函数。

闭包表达式语法
闭包表达式语法中的参数可以是in-out参数,但不能有默认值。如果命名可变参数,则可以使用可变参数。元组还可以用作参数类型和返回类型。

语法形式为:

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

从上下文推断类型
正在对字符串数组调用sorted(by:)方法,因此其参数必须是(String, String) -> Bool类型的函数。故上面代码可简化为:

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

单条表达式闭包隐式return
sorted(by:)方法参数的函数类型表明闭包必须返回Bool值。因为闭包的主体包含一个返回Bool值的表达式(s1>s2),所以不存在歧义,所以可以省略return关键字。

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

速记参数名
Swift自动为内联闭包提供速记参数名称,可以使用名称$0、$1、$2等来引用闭包参数的值。

如果在闭包表达式中使用这些速记参数名称,则可以从其定义中省略闭包的参数列表。速记参数名称的类型是从预期的函数类型推断出来的,并且您使用的编号最高的速记参数决定闭包采用的参数数量。in关键字也可以省略,因为闭包表达式完全由其主体组成。

reversedNames = names.sorted(by: { $0 > $1 } )

运算符方法
只需传入大于号运算符,Swift将推断你希望使用其特定于字符串的实现:

reversedNames = names.sorted(by: >)

尾随闭包
如果需要将闭包表达式作为函数的最终参数传递给函数,并且闭包表达式很长,那么将其作为尾随闭包编写会很有用。您可以在函数调用的括号后编写尾随闭包,即使尾随闭包仍然是函数的参数。使用尾随闭包语法时,不会将第一个闭包的参数标签作为函数调用的一部分写入。一个函数调用可以包括多个尾随闭包;但是,下面的前几个示例使用单个尾随闭包。

func someFunctionThatTakesAClosure(closure: () -> Void) {
    // function body goes here
}

// 调用函数(不用尾随闭包)
someFunctionThatTakesAClosure(closure: {
    // closure's body goes here
})

// 调用函数(用尾随闭包)
someFunctionThatTakesAClosure() {
    // trailing closure's body goes here
}

上面闭包表达式语法部分中的字符串排序闭包可以作为尾随闭包写入sort(by:)方法的括号之外:

reversedNames = names.sorted() { $0 > $1 }

如果将闭包表达式作为函数或方法的唯一参数提供,并且将该表达式作为尾随闭包提供,则在调用函数时,不需要在函数或方法的名称后写一对括号():

reversedNames = names.sorted { $0 > $1 }

当闭包足够长,无法在一行内联写入时,尾随闭包才最有用。例如,Swift的数组类型有一个map(_:)方法,该方法将闭包表达式作为其单个参数。对数组中的每个项调用一次闭包,并为该项返回一个可选的映射值(可能是其他类型的)。您可以通过在传递给map(_:)的闭包中编写代码来指定映射的性质和返回值的类型。
将提供的闭包应用于每个数组元素后,map(_:)方法返回一个包含所有新映射值的新数组,其顺序与其在原始数组中的对应值相同。

map(_:)函数的源码如下:

    /// Returns an array containing the results of mapping the given closure
    /// over the sequence's elements.
    ///
    /// In this example, `map` is used first to convert the names in the array
    /// to lowercase strings and then to count their characters.
    ///
    ///     let cast = ["Vivien", "Marlon", "Kim", "Karl"]
    ///     let lowercaseNames = cast.map { $0.lowercased() }
    ///     // 'lowercaseNames' == ["vivien", "marlon", "kim", "karl"]
    ///     let letterCounts = cast.map { $0.count }
    ///     // 'letterCounts' == [6, 6, 3, 4]
    ///
    /// - Parameter transform: A mapping closure. `transform` accepts an
    ///   element of this sequence as its parameter and returns a transformed
    ///   value of the same or of a different type.
    /// - Returns: An array containing the transformed elements of this
    ///   sequence.
    @inlinable public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]

下面介绍如何使用带有尾随闭包的map(_:)方法将Int值数组转换为字符串值数组。数组[16, 58, 510]用于创建新数组[“OneSix”, “FiveEight”, “FiveOneZero”]:

let digitNames = [
    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]

现在,可以使用numbers数组创建字符串值数组,方法是将闭包表达式作为尾随闭包传递给数组的map(_:)方法:

let strings = numbers.map { (number) -> String in
    var num = number
    var output = ""
    repeat {
        output = digitNames[num % 10]! + output
        num /= 10
    } while num > 0
    return output
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]

map(_:)方法为数组中的每个项调用一次闭包表达式。您不需要指定闭包的输入参数number的类型,因为可以从要映射的数组中的值推断该类型。

在本例中,使用闭包的num参数的值初始化变量number,以便可以在闭包主体内修改该值。(函数和闭包的参数始终是常量。)闭包表达式还指定字符串的返回类型,以指示将存储在映射输出数组中的类型。

为什么要在变量后加感叹号(!) ?
调用digitNames Dictionary的下标后会跟一个感叹号(!),因为Dictionary下标返回一个可选值,表示如果键不存在,Dictionary查找可能会失败。在上面的示例中,可以保证num%10始终是digitNames字典的有效下标键,因此使用感叹号强制展开存储在下标的可选返回值中的字符串值。

在上面的例子中,尾随闭包语法的使用在闭包支持的函数之后立即巧妙地封装了闭包的功能,而不需要将整个闭包包装在map(_:)方法的外圆括号中。

如果函数接受多个闭包,则忽略第一个尾随闭包的参数标签,并标记其余尾随闭包。例如,下面的功能为照片库加载图片:

func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}

调用此函数加载图片时,提供了两个闭包。第一个闭包是一个完成处理程序,它在成功下载后显示一张图片。第二个闭包是一个向用户显示错误的错误处理程序。

loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}

在本例中,loadPicture(from:completion:onFailure:)函数将其网络任务分派到后台,并在网络任务完成时调用两个完成处理程序之一。通过这种方式编写函数,可以将负责处理网络故障的代码与成功下载后更新用户界面的代码清晰地分开,而不是仅使用一个闭包来处理这两种情况。

捕获值
闭包可以从定义它周围的上下文中捕获常量和变量。然后,闭包可以从其主体中引用和修改这些常量和变量的值,即使定义常量和变量的原始范围已不存在。

在Swift中,可以捕获值的闭包的最简单形式是嵌套函数,它写在另一个函数体中。嵌套函数可以捕获其外部函数的任何参数,也可以捕获外部函数中定义的任何常量和变量。

下面是一个名为makeIncrementer的函数示例,它包含一个名为incrementer的嵌套函数。嵌套的incrementer()函数从其周围的上下文中捕获两个值runningTotal和amount。捕获这些值后,incrementer由makeIncrementer返回,作为每次调用runningTotal时按数量递增的闭包。

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

<!--create a second incrementer-->
let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

<!--call the original incrementer-->
incrementByTen()
// returns a value of 40

作为一种优化,Swift可能会捕获并存储一个值的副本,如果该值没有被闭包改变,并且在创建闭包后该值没有改变。

当不再需要变量时,Swift还会处理所有涉及到处理变量的内存管理。

如果将闭包指定给类实例的属性,并且闭包通过引用实例或其成员来捕获该实例,则将在闭包和实例之间创建一个强引用循环。Swift使用捕获列表来打破这些强引用循环。更多信息,参见Strong Reference Cycles for Closures.

闭包是引用类型
在上面的示例中,incrementBySeven和incrementByTen是常量,但是这些常量引用的闭包仍然能够增加它们捕获的runningTotal变量。这是因为函数和闭包是引用类型(reference types)。

无论何时将函数或闭包指定给常量或变量,实际上都是将该常量或变量设置为对函数或闭包的引用。在上面的例子中,incrementByTen所指的是闭包常量,而不是闭包本身的内容。

这也意味着,如果将闭包分配给两个不同的常量或变量,则这两个常量或变量都引用同一个闭包。

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50

incrementByTen()
// returns a value of 60

上面的示例显示调用alsoIncrementByTen与调用incrementByTen是相同的。因为它们都引用相同的闭包,所以它们都递增并返回相同的runningTotal。

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

闭包可以逃逸的一种方法是存储在函数外部定义的变量中。例如,许多启动异步操作的函数都将闭包参数作为完成处理程序。函数在启动操作后返回,但直到操作完成后才会调用闭包。闭包需要逃逸,稍后再调用。例如:

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

someFunctionWithEscapingClosure(_:)函数将闭包作为其参数,并将其添加到在函数外部声明的数组中。如果没有用@escaping标记此函数的参数,则会出现编译时错误:Converting non-escaping parameter 'completionHandler' to generic parameter 'Element' may allow it to escape,这时再去掉函数体语句,就不报错了。

self指当前对象。
如果self引用类的实例,则需要特别考虑引用self的逃逸闭包。在逃逸闭包中捕获self可以很容易地意外创建一个强引用循环。有关引用周期的信息,请参见Automatic Reference Counting。

通常,闭包通过在闭包体中使用变量来隐式捕获变量,但在这种情况下,您需要显式捕获变量。如果要捕获self,请在使用self时显式编写它,或者将self包含在闭包的捕获列表中。明确地写self可以让你表达你的意图,并提醒你没有一个引用循环。例如,在下面的代码中,传递给someFunctionWithEscapingClosure(_:)的闭包显式引用self。与此相反,传递给someFunctionWithNonescapingClosure(_:)的闭包是一个非逃逸闭包,这意味着它可以隐式引用self。

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()// 即执行非逃逸方法。如果去掉这行,下面先打印:10
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }// 隐式引用self
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)// 如果去掉函数体里的第二行代码,则输出:10
// Prints "200"

completionHandlers.first?()// 即,执行逃逸方法。即满足:“闭包逃逸了,稍后再调用”
print(instance.x)
// Prints "100"

如果去掉上面self.x = 100中的self.,则报错:Reference to property 'x' in closure requires explicit use of 'self' to make capture semantics explicit
如果去掉completionHandlers.first?()中的?,则报错:Value of optional type '(() -> Void)?' must be unwrapped to a value of type '() -> Void'

下面是doSomething()的一个版本,它通过将self包含在闭包的捕获列表中来捕获self,然后隐式引用self:

class SomeOtherClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { [self] in x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

如果self是结构或枚举的实例,则始终可以隐式引用self。然而,当self是结构或枚举的实例时,逃逸闭包无法捕获对self的可变引用。结构和枚举不允许共享可变性,正如在“Structures and Enumerations Are Value Types”中讨论的那样。

struct SomeStruct {
    var x = 10
    mutating func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }  // Ok
        someFunctionWithEscapingClosure { x = 100 }     // Error
    }
}

在上面的例子中,调用someFunctionWithEscapingClose函数是一个错误,因为它在一个mutating修饰的方法里面,所以self是可变的。这违反了逃逸闭包不能捕获结构对self的可变引用的规则。

使用 mutating 关键字修饰方法是为了能在该方法中修改 struct 或是 enum 的变量。

自动闭包
autoclosure是自动创建的闭包,用于包装作为参数传递给函数的表达式。它不接受任何参数,当调用它时,它返回包装在其中的表达式的值。这种语法上的便利让您可以通过编写一个普通表达式而不是显式闭包来省略函数参数周围的大括号。

调用自动闭包的函数很常见,但实现这种函数并不常见。例如,assert(condition:message:file:line:)函数将自动闭包其条件和消息参数;其条件参数仅在调试版本中计算,其消息参数仅在条件为false时计算。

autoclosure允许您延迟计算,因为在调用闭包之前,内部代码不会运行。延迟求值对于有副作用或计算开销大的代码很有用,因为它允许您控制代码求值的时间。下面的代码显示了闭包如何延迟评估。

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)// Prints "5"

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)// Prints "5"

print("Now serving \\(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)// Prints "4"

即使闭包内的代码删除了customersInLine数组的第一个元素,但在实际调用闭包之前,数组元素不会被删除。如果从不调用闭包,则闭包内的表达式永远不会求值,这意味着数组元素永远不会被删除。请注意,customerProvider的类型不是String,而是() -> String

当你将闭包作为参数传递给函数时,会得到延迟求值的相同行为。

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \\(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"

上面清单中的serve(customer:)函数采用显式闭包,返回客户的名称。

下面的serve(customer:)版本执行相同的操作,但它不是显式闭包,而是通过使用@autoclosure属性标记其参数的类型来进行自动闭包。现在,您可以调用该函数,就像它使用字符串参数而不是闭包一样。参数会自动转换为闭包,因为customerProvider参数的类型用@autoclosure属性标记。

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \\(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"

过度使用自动闭包会使代码难以理解。上下文和函数名应该清楚地表明评估被推迟。

如果想要自动闭包允许逃逸,则同时使用@autoclosure和@escaping属性。

// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))

print("Collected \\(customerProviders.count) closures.")
// Prints "Collected 2 closures."
for customerProvider in customerProviders {
    print("Now serving \\(customerProvider())!")
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"

在上面的代码中,collectCustomerProviders(_:)函数将闭包附加到customerProviders数组,而不是调用作为其customerProvider参数传递给它的闭包。数组在函数范围之外声明,这意味着可以在函数返回后执行数组中的闭包。因此,必须允许customerProvider参数的值逃逸函数的作用域。

以上是关于➽06闭包的主要内容,如果未能解决你的问题,请参考以下文章

➽06闭包

scala编程——函数和闭包

由于lambda闭包或调度程序问题,程序可能会挂起

Javascript代码片段在drupal中不起作用

javascript 匿名函数及闭包----转载

Swift 可选链在闭包中不起作用