swift 闭包

2021-04-01  本文已影响0人  明若晴空

闭包是自包含的功能块,可以在代码中传递和使用。Swift中的闭包类似于C和Objective-C中的 Block 以及其他编程语言中的lambdas。

闭包可以从定义它们的上下文中捕获和存储对任何常量和变量的引用。这被称为对这些常量和变量的闭合。Swift可以处理捕获的所有内存管理。

注意
如果我们不熟悉捕获的概念,请不要担心。下面将在捕获值中详细解释。

函数中引入的全局函数和嵌套函数实际上是闭包的特例。闭包有三种形式:

Swift的闭包表达式有一个干净、清晰的风格,通过优化,在常见场景中鼓励简洁、无混乱的语法。这些优化包括:

闭包表达式

嵌套函数中引入的嵌套函数是一种方便的方法,可以将自包含的代码块命名和定义在更大的函数中,成为更大函数的一部分。但是,有时不使用完整的声明和名称来编写类似函数结构的简短版本是很有用的。当我们使用一个将函数作为一个或多个参数的函数或方法时,尤其如此。

闭包表达式是一种以简短、集中的语法来编写内联闭包的方法。闭包表达式提供了一些语法优化,用于以缩写形式编写闭包,而不会失去清晰度或意图。下面的闭包表达式描述了,通过在几个迭代中细化sorted(by:)方法的示例来说明这些优化,每个迭代都以更简洁的方式表示相同的功能。

排序方法

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

下面的闭包表达式示例使用sorted(by:)方法对字符串值数组按逆字母顺序进行排序。以下是要排序的初始数组:

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

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

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

提供排序闭包的一种方法是编写正确类型的普通函数,并将其作为参数传递给sorted(by:)方法:

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"]

如果第一个字符串(s1)大于第二个字符串(s2),则backward(_:_:)函数将返回true,指示在排序数组中,s1应出现在s2之前。对于字符串中的字符,“大于”表示“在字母表中比较靠后出现”。这意味着字母“B”大于字母“A”,字符串“Tom”大于字符串“Tim”。它提供了一个相反的字母排序,“Barry”被放在“Alex”之前,以此类推。

然而,这是用一个相当冗长的方法来编写是一个单一的基本的表达式函数(a>b)。在本例中,最好可以使用闭包表达式语法来内联编写排序闭包。

闭包表达式语法

闭包表达式语法具有以下一般形式:


截屏2021-02-25 下午4.24.44.png

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

下面的示例显示了上面的backward(_:_:)函数的闭包表达式版本:

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

请注意,此内联闭包的参数和返回类型的声明与来自backward(_:_:)函数的声明相同。在这两种情况下,它都写为(s1: String, s2: String) -> Bool。但是,对于内联闭包表达式,参数和返回类型写在花括号内,而不是在花括号外。

闭包主体的开头由in关键字引入。这个关键字表示闭包的参数和返回类型的定义已经完成,并且闭包的主体即将开始。

因为闭包的主体很短,甚至可以写在一行上:

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

这说明对sorted(by:)方法的整体调用保持不变。一对圆括号仍然包装方法的整个参数。然而,这个参数现在是一个内联闭包。

从上下文推断类型

因为排序闭包是作为参数传递给方法的,所以Swift可以推断其参数的类型和返回值的类型。对字符串数组上调用sorted(by:)方法,因此其参数必须是(String, String) -> Bool类型的函数。这意味着(String,String)和Bool类型不需要作为闭包表达式定义的一部分编写。因为可以推断出所有类型,所以返回箭头(->)和参数名称周围的括号也可以省略:

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

将闭包作为内联闭包表达式传递给函数或方法时,总是可以推断参数类型和返回类型。因此,当闭包用作函数或方法参数时,不需要以最完整的形式编写内联闭包

尽管如此,如果我们愿意,我们仍然可以使类型显式化。如果这样做可以避免代码读者的歧义,那么我们鼓励我们这样做。在sorted(by:)方法中,很明显,闭包的目的就是进行排序,读者可以安全地假设闭包可能与String类型的值一起工作,因为它有助于对字符串数组进行排序。

单表达式闭包的隐式返回

通过在声明中省略return关键字,单表达式闭包可以隐式返回其单表达式的结果,如前一示例的此版本中所示:

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

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

简写参数名称

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

如果在闭包表达式中使用这些简写参数名称,则可以从其定义中省略闭包的参数列表,简写参数名称的数量和类型将从预期的函数类型中推断出来。in关键字也可以省略,因为闭包表达式完全由其主体组成:

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

这里,$0和$1引用闭包的第一个和第二个字符串参数。

运算符方法

实际上,还有一种更短的方法来编写上面的闭包表达式。Swift的String类型将大于运算符(>)特定于字符串的实现定义为一个方法,该方法有两个String类型的参数,并返回Bool类型的值。这与sorted(by:)方法所需的方法类型完全匹配。因此,我们只需传入大于运算符,Swift将推断我们希望使用大于运算符特定于字符串的实现:

reversedNames = names.sorted(by: >)

有关运算符方法的详细信息,请参见运算符方法

尾部闭包

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

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

// Here's how you call this function without using a trailing closure:

someFunctionThatTakesAClosure(closure: {
    // closure's body goes here
})

// Here's how you call this function with a trailing closure instead:

someFunctionThatTakesAClosure() {
    // trailing closure's body goes here
}

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

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

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

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

当闭包足够长以至于无法在一行内联写入时,尾部闭包最有用。例如,Swift的数组类型有一个map(_:)方法,该方法将闭包表达式作为其单个参数。对数组中的每个项调用一次闭包,并为该项返回一个可选的映射值(可能是其他类型的)。通过在传递给map(_:)的闭包中编写代码,可以指定映射的性质和返回值的类型。

将提供的闭包应用于每个数组元素后,map(_:)方法返回一个新数组,其中包含所有新映射的值,其顺序与原始数组中相应的值相同。

下面是如何使用带有尾部闭包的map(_:)方法将Int值数组转换为String类型的数组。数组[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 number = number
    var output = ""
    repeat {
        output = digitNames[number % 10]! + output
        number /= 10
    } while number > 0
    return output
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]

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

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

每次调用闭包表达式时,它都会生成一个名为output的字符串。它使用余数运算符(number % 10)计算数字的最后一位,并使用此数字在digitNames字典中查找适当的字符串。这个闭包可用于创建任何大于零的整数的字符串表示形式。

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

digitNames字典中检索到的字符串被添加到output的前面,从而有效地反向构建数字的字符串版本。(表达式number % 10为16给出的值6,为58给出8,为510给出0。)

然后将数字变量除以10。因为它是一个整数,在除法过程中会向下舍入,所以16变成1,58变成5,510变成51。

重复该过程,直到number等于0,此时闭包返回输出字符串,并通过map(_:)方法将其添加到输出数组中。

在上面的例子中,尾部闭包语法的使用巧妙地将闭包的功能封装在闭包所支持的函数之后,而不需要将整个闭包包装在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()数从其周围上下文中捕获两个值runningTotalamount。在捕获这些值之后,incrementermakeIncrementer作为一个闭包返回,每次调用runningTotal时都按数量递增。

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

makeIncrementer的返回类型是() -> Int。这意味着它返回一个函数,而不是一个简单的值。它返回的函数没有参数,每次调用时都返回一个Int值。要了解函数如何返回其他函数,请参见函数类型作为返回类型

makeIncrementer(forIncrement:)函数定义了一个名为runningTotal的整数变量,用于存储将返回的incrementer的当前运行总数。此变量的初始化值为0。

makeIncrementer(forIncrement:)函数有一个Int参数,参数标签为forIncrement,参数名称为amount。传递给此参数的参数值指定l了每次调用返回的incrementer函数时runningTotal的增量。makeIncrementer函数定义了一个名为incrementer的嵌套函数,该函数执行实际的递增操作。此函数只是将amount添加到runningTotal,并返回结果。

如果单独考虑,嵌套的incrementer()函数可能看起来不寻常:

func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}

incrementer()函数没有任何参数,但它引用了函数体中的runningTotalamount。它通过从周围函数捕获对runningTotalamount的引用并在自己的函数体中使用它们来实现这一点。通过引用捕获可以确保在调用makeIncrementer结束时,runningTotalamount不会消失,还可以确保runningTotal在下次调用incrementer函数时可用。

注意
作为一种优化,如果该值没有被闭包改变,并且在创建闭包后该值没有改变,Swift可以捕获并存储一个值的副本。
当变量不再需要时,Swift也会处理所有涉及到处理变量的内存管理。

下面是makeIncrementer的一个实例:

let incrementByTen = makeIncrementer(forIncrement: 10)

本例设置了一个名为incrementByTen的常量,以引用一个incrementer函数,该函数在每次调用它的runningTotal变量时将其加上10。多次调用函数将显示此行为:

incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

如果创建第二个递增器,它将有自己对新的、单独的runningTotal变量的存储引用(不会复用之前已经创建的递增器的引用)

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

再次调用原始递增器(incrementByTen)将继续递增其自身的runningTotal变量,并且不影响incrementBySeven捕获的变量:

incrementByTen()
// returns a value of 40

注意
如果将闭包分配给类实例的属性,并且闭包通过引用实例或实例的成员来捕获该实例,那么将在闭包和实例之间创建一个强引用循环。Swift使用捕获列表来打破这些强引用循环。有关更多信息,请参见闭包的强引用循环

闭包是引用类型

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

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

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

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

incrementByTen()
// returns a value of 60

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

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

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

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

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

如果self引用了类的实例,则需要特别考虑引用self的转义闭包。在转义闭包中捕捉self,很容易不小心创建一个强引用循环。有关引用循环的信息,请参阅自动引用计数

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

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

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"

completionHandlers.first?()
print(instance.x)
// Prints "100"

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

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

如果self是结构或枚举的实例,则始终可以隐式引用self。但是,当self是结构或枚举的实例时,转义闭包无法捕获对self的可变引用。结构和枚举不允许共享可变性,正如在结构和枚举是值类型中讨论的那样。

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

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

自动闭包

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

调用采用autoclosure的函数很常见,但实现这种函数并不常见。例如,assert(condition:message:file:line:)函数将autoclosure作为其conditionmessage参数;仅在调试生成中计算其condition参数,仅在条件为false时计算其message参数。

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属性标记其参数的类型来采用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会使代码难以理解。上下文和函数名应该清楚地表明正在延迟计算。

如果想用允许转义的autoclosure,请同时使用@autoclosure@escaping属性。@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参数的值转义h函数的作用域。

上一篇下一篇

猜你喜欢

热点阅读