Swift里的闭包

Swift 中的闭包是自包含的函数代码块,可以捕获其上下文中的值。闭包表达式提供了一种简洁的语法,允许根据上下文推断类型、单表达式闭包的隐式返回等。闭包可以作为参数传递,如在排序方法中,通过闭包决定元素的排列顺序。尾随闭包可以使代码更紧凑,而逃逸闭包则允许闭包在定义它的函数返回后仍能被调用。闭包是引用类型,即使常量引用的闭包也能改变其捕获的变量值。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift 中的闭包与 C 和 Objective-C 中的代码块(blocks)以及其他一些编程语言中的匿名函数(Lambdas)比较相似。

闭包采用如下三种形式之一:

  • 全局函数是一个有名字但不会捕获任何值的闭包
  • 嵌套函数是一个有名字并可以捕获其封闭函数域内值的闭包
  • 闭包表达式是一个利用轻量级语法所写的可以捕获其上下文中变量或常量值的匿名闭包

闭包表达式

闭包表达式是一种构建内联闭包的方式,它的语法简洁。在保证不丢失它语法清晰明了的同时,闭包表达式提供了几种优化的语法简写形式。

下面通过对 sorted(by:) 这一个案例的多次迭代改进来展示这个过程

排序方法

Swift 标准库提供了名为 sorted(by:) 的方法,它会基于你提供的排序闭包表达式的判断结果对数组中的值(类型确定)进行排序。一旦它完成排序过程,sorted(by:) 方法会返回一个与旧数组类型大小相同类型的新数组

sorted(by:) 方法接受一个闭包,该闭包函数需要传入与数组元素类型相同的两个值,并返回一个布尔类型值来表明当排序结束后传入的第一个参数排在第二个参数前面还是后面。如果第一个参数值出现在第二个参数值前面,排序闭包函数需要返回 true,反之返回 false

例如:对一个 String 类型的数组进行排序,因此排序闭包函数类型需为 (String, String) -> Bool

// 1、使用 sorted 排序数组
let fruits = ["Peach","Banana","Apple","Melon","grape"] //Ascll a:97  A:65
var sortedFruits = fruits.sorted() //sorted 默认从小到大 排列
print(sortedFruits) //["Apple", "Banana", "Melon", "Peach", "grape"]


//2、 比较逻辑函数 
func comparison(_ a: String, _ b: String) -> Bool { //从大到小排列 对于字符串中的字符来说,“大于”表示“按照字母顺序较晚出现”。
    return a > b  // 返回为 true 时 a 排序在 b 前
}

//3、闭包传入 比较逻辑函数
var sortedFruitsName  = fruits.sorted(by: comparison)
print(sortedFruitsName) //["grape", "Peach", "Melon", "Banana", "Apple"]

闭包表达式语法

闭包表达式语法有如下的一般形式:

{ (parameters) -> return type in
    statements
}

闭包表达式参数 可以是 in-out 参数,但不能设定默认值。如果你命名了可变参数,也可以使用此可变参数。元组也可以作为参数和返回值

对应的闭包表达式版本的代码:

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

内联闭包参数和返回值类型声明与 sortedFruitsName(_:_:) 函数类型声明相同。在这两种方式中,都写成了 (s1: String, s2: String) -> Bool。然而在内联闭包表达式中,函数和返回值类型都写在大括号内,而不是大括号外

闭包的函数体部分由关键字 in 引入。该关键字表示闭包的参数和返回值类型定义已经完成,闭包函数体即将开始。

//改写成一行代码:
sortedFruitsName = fruits.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )

根据上下文推断类型

Swift 可以推断其参数和返回值的类型。

//排序闭包函数是作为 sorted(by:) 方法的参数传入的,Swift 可以推断其参数和返回值的类型。
sortedFruitsName = fruits.sorted(by: { (s1, s2) -> Bool in
    return s1 > s2
})

//参数必须是 (String, String) -> Bool 类型的函数。这意味着 (String, String) 和 Bool 类型并不需要作为闭包表达式定义的一部分
//返回箭头(->)和围绕在参数周围的括号也可以被省略
sortedFruitsName = fruits.sorted(by: { s1, s2 in return s1 > s2 })

通过内联闭包表达式构造的闭包作为参数传递给函数或方法时,总是能够推断出闭包的参数和返回值类型。这意味着闭包作为函数或者方法的参数时,你几乎不需要利用完整格式构造内联闭包。

明确写出有着完整格式的闭包。如果完整格式的闭包能够提高代码的可读性,则我们更鼓励采用完整格式的闭包

单表达式闭包的隐式返回

//单行表达式闭包可以通过省略 return 关键字来隐式返回单行表达式的结果,
sortedFruitsName = fruits.sorted(by: { s1, s2 in s1 > s2 })

参数名称缩写

Swift 自动为内联闭包提供了参数名称缩写功能,你可以直接通过 $0$1$2 来顺序调用闭包的参数,以此类推

//闭包表达式中使用参数名称缩写,你可以在闭包定义中省略参数列表,
并且对应参数名称缩写的类型会通过函数类型进行推断。
in 关键字也同样可以被省略,因为此时闭包表达式完全由闭包函数体构成:

sortedFruitsName = fruits.sorted(by: { $0 > $1 })
//这个例子中,$0 和 $1 表示闭包中第一个和第二个 String 类型的参数。

运算符方法

Swift 的 String 类型定义了关于大于号(>)的字符串实现,其作为一个函数接受两个 String 类型的参数并返回 Bool 类型的值。而这正好与 sorted(by:) 方法的参数需要的函数类型相符合。因此,你可以简单地传递一个大于号,Swift 可以自动推断找到系统自带的那个字符串函数的实现

sortedFruitsName = fruits.sorted(by: >)

尾随闭包

尾随闭包是一个书写在函数圆括号之后的闭包表达式,函数支持将其作为最后一个参数调用。在使用尾随闭包时,你不用写出它的参数标签:

Swift 的 Array 类型有一个 map(_:) 方法,这个方法获取一个闭包表达式作为其唯一参数。

数组 map(_:) 方法将返回一个新的数组,数组中包含了与原数组中的元素一一对应的映射后的值

下例介绍了如何在 map(_:) 方法中使用尾随闭包将 Int 类型数组 [16, 58, 510] 转换为包含对应 String 类型的值的数组 ["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 //number 变量之后除以 10。因为其是整数,在计算过程中未除尽部分被忽略
    } while number > 0
    return output
}
// strings 常量被推断为字符串类型数组,即 [String]
// 其值为 ["OneSix", "FiveEight", "FiveOneZero"]

字典 digitNames 下标后跟着一个叹号(!),因为字典下标返回一个可选值(optional value),表明该键不存在时会查找失败。在上例中,由于可以确定 number % 10 总是 digitNames 字典的有效下标,因此叹号可以用于强制解包(force-unwrap)存储在下标的可选类型的返回值中的 String 类型的值

值捕获

闭包可以在其被定义的上下文中捕获常量或变量。即使定义这些常量和变量的原作用域已经不存在,闭包仍然可以在闭包函数体内引用和修改这些值

//返回值为 ()->Int 函数类型闭包
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

makeIncrementer(forIncrement:) 有一个 Int 类型的参数,其外部参数名为 forIncrement,内部参数名为 amount,该参数表示每次 incrementer 被调用时 runningTotal 将要增加的量。makeIncrementer 函数还定义了一个嵌套函数 incrementer,用来执行实际的增加操作。该函数简单地使 runningTotal 增加 amount,并将其返回。

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

incrementer() 函数并没有任何参数,但是在函数体内访问了 runningTotal 和 amount 变量。这是因为它从外围函数捕获了 runningTotal 和 amount 变量的引用。捕获引用保证了 runningTotal 和 amount 变量在调用完 makeIncrementer 后不会消失,并且保证了在下一次执行 incrementer 函数时,runningTotal 依旧存在

注意 为了优化,如果一个值不会被闭包改变,或者在闭包创建后不会改变,Swift 可能会改为捕获并保存一份对值的拷贝。 Swift 也会负责被捕获变量的所有内存管理工作,包括释放不再需要的变量。 (原理 ??)

使用 makeIncrementer 的例子:

let incrementByTen = makeIncrementer(forIncrement: 10)
print(incrementTen()) //10
print(incrementTen()) //20
print(incrementTen()) //30
print(incrementTen) //(Function)

let incrementSeven = makeIncrementer(forIncrement: 7)
print(incrementSeven()) //7
print(incrementTen())   //40
    

闭包是引用类型

incrementBySeven 和 incrementByTen 都是常量,但是这些常量指向的闭包仍然可以增加其捕获的变量的值。这是因为函数和闭包都是引用类型

如果你将闭包赋值给了两个不同的常量或变量,两个值都会指向同一个闭包:

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 返回的值为50

*指针的应用呗,都指向的是同一个函数变量的指针( )

逃逸闭包

当一个闭包作为参数传到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。当你定义接受闭包作为参数的函数时,你可以在参数名之前标注 @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
func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        //将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 self。
        
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

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

print(completionHandlers)//[(Function)] completionHandlers里存的是有逃逸函数的赋值方法

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

自动闭包

自动闭包是一种自动创建的闭包,用于包装传递给函数作为参数的表达式。这种闭包不接受任何参数,当它被调用的时候,会返回被包装在其中的表达式的值。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包

我们经常会调用采用自动闭包的函数,但是很少去实现这样的函数。举个例子来说,assert(condition:message:file:line:) 函数接受自动闭包作为它的 condition 参数和 message 参数;

自动闭包让你能够延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些有副作用(Side Effect)和高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。下面的代码展示了闭包如何延时求值。

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

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

print("Now remove \(customerProvider())!")
// Prints "Now remove Chris!"

print(customersInLine.count)
// 打印出 "4"

尽管在闭包的代码中,customersInLine 的第一个元素被移除了,不过在闭包被调用之前,这个元素是不会被移除的。如果这个闭包永远不被调用,那么在闭包里面的表达式将永远不会执行,那意味着列表中的元素永远不会被移除。customerProvider 的类型不是 String,而是 () -> String,一个没有参数且返回值为 String 的函数。

将闭包作为参数传递给函数时,你能获得同样的延时求值行为。

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

下面这个版本的 serve(customer:) 完成了相同的操作,不过它并没有接受一个显式的闭包,而是通过将参数标记为 @autoclosure 来接收一个自动闭包。现在你可以将该函数当作接受 String 类型参数(而非闭包)的函数来调用。customerProvider 参数将自动转化为一个闭包,因为该参数被标记了 @autoclosure 特性。

//通过将参数标记为 @autoclosure 来接收一个自动闭包。
可以理解将该函数当作接受 String 类型参数(而非闭包)的函数来调用

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

不理解与上面的有啥区别。??

如果你想让一个自动闭包可以“逃逸”,则应该同时使用 @autoclosure 和 @escaping 属性。@escaping 属性


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.")
// 打印 "Collected 2 closures."

for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// 打印 "Now serving Barry!"
// 打印 "Now serving Daniella!"

参考 感谢:swift学习笔记  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值