Swift基础入门知识学习(11)-闭包-第一篇-讲给你懂

Swift基础入门知识学习(10)-函数(函式)-讲给你懂
超速学习-重点笔记


理解难度
★★★★★
实用程度
★☆☆☆☆

打起精神,因为这里要说明在Swift教程中很多人不容易理解的第一件事:闭包(closure)。

闭包是Swift最强大的功能之一,但也是很容易让人困惑的功能。你会发现它们很难!不用担心,这代表你的大脑正在正常运作。

到底什么时候会用上闭包呢?以下是你可能会用到的情况:

  1. 延迟后运行一些代码。
  2. 动画完成后运行一些代码。
  3. 下载完成后运行一些代码。
  4. 当用户从菜单中选择一个选项时,运行一些代码。

闭包让我们将一些功能包在一个变量中,然后存储在某个地方。我们也可以从函数返回它,并将闭包存储在其他地方。

闭包(closure)简单来讲就是一个匿名函数,与普通函数同样为一个独立的代码区块,可以在代码中被传递和使用。闭包的特性为能够捕获和储存在其前后文中任何常量与变量,进而独立的执行工作。

闭包有点难以阅读——特别是当它们接受和/或返回自己的参数时。但没关系:这迈出的一小步,如果你卡住了,再看一遍,多看几遍…没事的。

闭包有三种表现方式:

  • 函数章节中提到的全局函数(全域函式)是一种有名称但不会捕获任何值的闭包。
  • 嵌套函数(巢状函式)(nested function)是一种有名称且被包含在另一个函数(即上层函数)中的闭包,嵌套函数(巢状函式)可以从上层函数中捕获值。
  • 闭包表达式就是使用简洁语法来描述的一种没有名称的闭包,可以在代码中被传递和使用,特性为能够捕获和储存在其前后文中任何常量与变量的参数,进而独立地执行工作。

闭包表达式

最简单的示例:


let 闭包 = {
    
    

    print("这就是一个闭包")
}

闭包()//调用

这样就创建了一个没有名称的函数,并将该函数分配给“闭包”。你现在可以像调用一般函数一样调用“闭包( )”。

闭包的本质就是代码块,它是函数的升级版;函数是有名称、可复用的代码块,闭包则是比函数更加灵活的匿名代码块。

闭包与大括号

闭包和函数都可以取参数,但它们取参数的方式大不相同。以下是一个接受字符串和整数的函数:

func pay(user: String, amount: Int) {
    
    
    // code
}

以下是与闭包完全相同的内容:

let payment = {
    
     (user: String, amount: Int) in
    // code
}

如你所见,参数已移动到大括号内,in关键字用于标记参数列表的结束和闭包主体本身的开始。

闭包表达式(closure expression)是一种利用简洁语法建立匿名函数的方式。同时也提供了一些优化语法,可以使得代码码变得更好懂及直觉。闭包表达式的格式如下:

{ (参数) -返回值类型 in
内部执行的代码
}

闭包将参数放在大括号内,以避免混淆Swift:如果我们写了let payment = (user: String, amount: Int)那么看起来我们试图创建一个元组,而不是闭包,那会有问题。

在大括号内包含参数列表说明了为什么 in 关键字如此重要——没有它,Swift很难知道你的闭包主体的实际起点,因为没有第二组大括号了。

在闭包中接受参数

例如,我们可以像这样制作一个接受地名字符串作为其唯一参数的闭包:

let driving = {
    
     (place: String) in

    print("I'm going to \(place) in my car")
    
}

函数和闭包之间的区别之一是,在运行闭包时不使用参数标签。因此,为了调用driving(),我们现在要写以下内容:

driving("London")

上述代码中可以看到,与函数相同是以大括号{ }将代码包起来,但省略了名称,包着参数的小括号( )摆到{ }里并接着箭头->及返回值类型。然后使用in分隔内部执行的代码。

闭包表达式可以使用常量、变量和inout类型作为参数,但不能有预设值。也可以在参数列表的最后使用可变量量参数(variadic parameter)。元组也可以作为参数和返回值。

下面是一个例子,从将一个函数当做另一个函数的参数开始,原始代码如下:

// 这是一个要当做参数的函数 功能为将两个传入的参数整数相加并返回
func addTwoInts(number1: Int, number2: Int) -> Int {
    
    

    return number1 + number2
    
}

// 建立另一个函数,有三个参数依序为
// 类型为 (Int, Int) -> Int 的函数, Int, Int
func printMathResult(

 _ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
    
    

    print("Result: \(mathFunction(a, b))")
    
}

这边将函数addTwoInts()修改成一个匿名函数(即闭包)传入,代码如下:

// 呼叫 printMathResult() 函数 参数分别为 闭包, Int, Int
printMathResult({
    
    (number1: Int, number2: Int) -> Int in

   return number1 + number2

}, 12, 85)

// 打印出:97

/* 第一个参数为一个匿名函数(闭包) 如下
{(number1: Int, number2: Int) -> Int in
   return number1 + number2
}
*/

上述代码中可以看到函数printMathResult()依旧为三个参数,第一个参数原本应该是一个函数,但可以简化成一个闭包并直接传入,其后为两个Int的参数。

接下来会使用上面这个例子,介绍几种优化语法的方式,越后面介绍的代码语法会越简洁,但使用上功能是一样的。

根据前后文推断类型

因为两数相加闭包是作为函数printMathResult()的参数传入的,Swift 可以自动推断其参数及返回值的类型(根据建立函数printMathResult()时的参数类型(Int, Int) -> Int),因此这个闭包的参数及返回值的类型都可以省略,同时包着参数的小括号()及返回箭头->也可以省略,修改如下:

printMathResult(
  {
    
    number1, number2 in 
return number1 + number2}, 12, 85)

// 打印出:97

/* 第一个参数修改成如下
{number1, number2 in return number1 + number2}
*/

闭包返回值

闭包也可以返回值,它们的编写方式与参数相似:你将它们写入闭包中,直接in关键字前面。

为了证明这一点,我们将关闭我们的driving()并让它返回其价值,而不是直接打印它。

原来是这样打印:

let driving = {
    
     (place: String) in

    print("I'm going to \(place) in my car")
    
}

现在希望返回一个字符串而不是直接打印的闭包,因此我们需要in之前使用 -> String 表示返回的类型,然后像一般函数一样使用return:

let drivingWithReturn = {
    
     (place: String) -> String in

    return "I'm going to \(place) in my car"
    
}

然后可以运行该闭包并打印其返回值:

let message = drivingWithReturn("London")

print(message)

单表达式闭包隐式回传

单行表达式闭包可以通过隐藏return来隐式回传单行表达式的结果,修改如下:

printMathResult({
    
    number1, number2 in 

number1 + number2}, 12, 85)
// 打印出:97

/* 第一个参数修改成如下
{number1, number2 in number1 + number2}
*/

参数简称

Swift 能够自动为闭包提供参数简称的功能,可以直接以$0,$1,$2这种方式来依序呼叫闭包的参数。

如果使用了参数简称,就可以省略在闭包参数列表中对其的定义,且对应参数简称的类型会通过函数类型自动进行推断,所以同时in也可以被省略,修改如下:

printMathResult({
    
    $0 + $1}, 12, 85)
// 打印出:97

/* 第一个参数修改成如下
{$0 + $1}
*/

什么时候应该使用参数简称?

以下三点请考虑:

  1. 参数很多吗?如果是这样,参数简称就最好别用。因为这会适得其反!如果你需要将$3还是$4与$0进行判断,给他们一个实际的名称,代码就会变得更加清晰。
  2. 该功能是常用的吗?随着你的Swift技能的进步,你会开始意识到,有些人会使用闭包的常见函数。所以你的代码要考虑到其他人可以轻松理解$0的含义。
  3. 在你的方法中使用了多次参数简称吗?如果你需要引用$0超过两三次,也许你应该给它一个真名。

再次强调重要的是,你的代码需要易于阅读和理解;至少你自己下一次再看,都能够一眼就看明白。虽然有时要简化代码,但并不总是漫无目的的简化。需要根据具体情况来优化和简化代码。

运算子函数

实际上还有一种更简洁的语法。Swift 的String类型定义了关于加号+的字串实作,其作为一个函数接受两个数值,并返回这两个数值相加的值。而这正好与最一开始的函数addTwoInts()相同,因此你可以简单地传入一个加号+,Swift会自动推断出加号的字串函数实作,修改如下:

printMathResult(+, 12, 85)
// 打印出:97

// 第一个参数修改成: +

以上介绍了四种优化闭包的语法,使用上功能都与最一开始的闭包相同,所以可以依照需求以及合适性,使用不同的优化语法,来让你的代码更简洁与直觉,当然都使用完整写法的闭包也是可以的。

尾随闭包

如果需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包(trailing closure)来加强函数的可读性。尾随闭包是一个写在函数括号()之后的闭包表达式,函数支援将其作为最后一个参数呼叫。以下是一个例子:

参数为闭包的函数

func someFunction(closure: () -> Void) {
    
    
    // 内部执行的代码
}
// 内部参数名称为 closure
// 闭包的类型为 () -> Void 没有参数也没有返回值

不使用尾随闭包的函数

someFunction(closure: {
    
    
    // 闭包内的代码
})
// 可以看到这个闭包作为参数 是放在 () 里面

使用尾随闭包的函数

someFunction() {
    
    
  // 闭包内的代码
}
// 可以看到这个闭包作为参数 位置在 () 后空一格接着写下去

如果函数只有闭包这一个参数时,甚至可以将函数的()省略,修改如下:

// 使用尾随闭包进行函数呼叫 省略函数的 ()
someFunction {
    
    
  // 闭包内的代码
}
let names = ["AT", "AE", "D", "S", "BE"]

//尾随闭包
var reversed = names.sorted() {
    
     $0 > $1 }
print(reversed)

捕获值

闭包可以在其定义的前后文中捕获(capture)常量或变量,即使定义这些常量或变量的原使用区域已经不存在,闭包仍可以在闭包函数体内修改这些值。

Swift 中,可以捕获值的闭包的最简单形式是嵌套函数(巢状函式),也就是定义在其他函数内的函数。嵌套函数(巢状函式)可以捕获并存取上层函数(把它定义在其中的函数)内所有的参数以及定义的常量与变量,即使这个嵌套函数(巢状函式)已经回传,导致常量或变量的作用范围不存在,闭包仍能对这些已经捕获的值做操作。

以下是一个例子:

// 定义一个函数 参数是一个整数 回传是一个类型为 () -> Int 的闭包
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    
    
    // 用来储存计数总数的变量
    var runningTotal = 0

    // 嵌套函数(巢状函式) 简单的将参数的数字加进计数并返回
    // runningTotal 和 amount 都被捕获了
    func incrementer() -> Int {
    
    
        runningTotal += amount
        return runningTotal
    }

    // 返回捕获变量参考的嵌套函数(巢状函式)
    return incrementer
}

一个函数makeIncrementor ,它有一个Int型的参数amout, 并且它有一个外部参数名字forIncremet,意味着你调用的时候,必须使用这个外部名字。返回值是一个()-> Int的函数。

内部存取了的runningTotal及amount变量,是因为它从外部函数捕获了这两个变量的参考。而这个捕获参考会让runningTotal与amount在呼叫完makeIncrementer函数后不会消失,并且下次呼叫incrementer函数时,runningTotal仍会存在。

以下是呼叫这个函数的例子:

// 宣告一个常量
// 会被指派为一个每次呼叫就会将 runningTotal 加 10 的函数 incrementer
let incrementByTen = makeIncrementer(forIncrement: 10)
// 呼叫多次 可以观察到每次返回值都是累加上去
incrementByTen() // 10
incrementByTen() // 20
incrementByTen() // 30

// 如果另外再宣告一个常量
// 会有属于它自己的一个全新独立的 runningTotal 变量参考
// 与上面的常量无关
let incrementBySix = makeIncrementer(forIncrement: 6)
incrementBySix() // 6
incrementBySix() // 12

// 第一个常量仍然是对它自己捕获的变量做操作
incrementByTen() // 40

闭包是参考类型

前面的例子中,incrementByTen和incrementBySix是常量,但这些常量指向的闭包仍可以增加其捕获的变量值,这是因为函数与闭包都是参考类型。

参考类型就是无论将函数(或闭包)指派给一个常量或变量,实际上都是将常量或变量的值设置为对应这个函数(或闭包)的参考(参考其在记忆体空间内配置的位置)。

所以当你将闭包指派给了两个不同的常量或变量,这两个值都会指向同一个闭包(的参考),如下:

// 指派给另一个常量
let alsoIncrementByTen = incrementByTen

// 仍然是对原本的 runningTotal 操作
alsoIncrementByTen() // 50

逃逸闭包

当一个闭包被当做参数传入一个函数中,但是这个闭包在函数返回后才被执行(例如像是闭包被当做函数的返回值,然后接着被拿去做别的操作),这样称作闭包从函数中逃逸(escape)。你可以在参数类型前面加上@escaping来明确标注这个闭包是可以逃逸的。例子如下:

// 参数为一个闭包的函数 参数类型前面标注 @escaping
var completionHandlers: [() -> Void] = []

func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    
    

    completionHandlers.append(completionHandler)
    
}

上述代码可以看到函数someFunctionWithEscapingClosure()将一个闭包当作参数传入,并将这个闭包加入到一个定义好的阵列,以将这个阵列(与其内的闭包们)作为后续使用,这时如果没有在参数类型前面加上@escaping,则会遇到编译时错误。

另外还有一点,将闭包标注为@escaping,表示你必须在闭包中显式地参考self,而非逃逸的闭包可以隐式地参考self(例如原本应该写self.x的,可以简化写成x,因为可以隐式参考self,会自动推断为self的x属性),例子如下:

// 定义另一个[参数不为逃逸的闭包]的函数
func someFunctionWithNonescapingClosure(closure: () -> Void) {
    
    

    closure()
    
}

// 定义一个类别
class SomeClass {
    
    

    var x = 10
    
    func doSomething() {
    
    
        // 使用到前面定义的两个函数 都使用了尾随闭包来让语法更为简洁
        // 传入当参数的闭包 内部都是将实体的属性指派为新的值

        // 参数类型标注为 @escaping 的闭包
        // 需要显式地参考 self
        someFunctionWithEscapingClosure {
    
     self.x = 100 }

        // 而为非逃逸的闭包
        // 其内可以隐式地参考 self
        someFunctionWithNonescapingClosure {
    
     x = 200 }
        
    }
}

// 生成一个实体
let instance = SomeClass()

// 呼叫其内的方法
instance.doSomething()
// 接着那两个前面定义的函数都会被呼叫到 所以最后实体的属性 x 为 200
print(instance.x)

// 接着呼叫阵列中的第一个成员
// 也就是示范逃逸闭包的函数中 会将闭包加入阵列的这个动作
// 而这个第一个成员就是 { self.x = 100 }
completionHandlers.first?()
// 所以这时实体的属性 x 便为 100
print(instance.x)

自动闭包

自动闭包(autoclosure)是一种自动被建立的闭包,用于包装后传递给函数作为参数的表达式。这种闭包没有参数,而当被使用时,会返回被包装在其内的表达式的值。

也就是说,自动闭包是一种简化的语法,让你可以用一个普通的表达式代替显式的闭包,进而省略了闭包的大括号{ }。

自动闭包让你可以延迟求值,因为这个闭包会直到被你呼叫时才会执行其内的代码,以下先示范一个普通的闭包如何延迟求值:

// 首先宣告一个有五个成员的阵列
var customersInLine = ["Albee","Alex","Eddie","Zack","Kevin"]

// 打印出:5
print(customersInLine.count)

// 接着宣告一个闭包 会移除掉阵列的第一个成员
let customerProvider = {
    
     customersInLine.remove(at: 0) }

// 这时仍然是打印出:5
print(customersInLine.count)

// 直到这个闭包被呼叫时 才会执行
// 打印出:开始移除 Albee !
print("开始移除 \(customerProvider()) !")

// 这时就只剩下 4 个成员了 打印出:4
print(customersInLine.count)

上述代码可以看到闭包直到被呼叫时,才会移除成员,所以如果不呼叫闭包的话,则阵列成员都不会被移除。另外要注意一点,这个闭包customerProvider的类型为() -> String,而不是String。

将闭包作为参数传递给函数时,一样可以延迟求值,如下:

// 这时 customersInLine 为 ["Alex", "Eddie", "Zack", "Kevin"]

// 定义一个[参数为闭包]的函数
func serve(customer customerProvider: () -> String) {
    
    
    // 函数内部会呼叫这个闭包
    print("开始移除 \(customerProvider()) !")
}

// 呼叫函数时 [移除阵列第一个成员]这个动作被当做闭包的内容
// 闭包被当做参数传入函数
// 这时才会移除阵列第一个成员
serve(customer: {
    
     customersInLine.remove(at: 0) } )

接着则介绍如何使用自动闭包完成上述一样的动作。你必须在参数类型前面标注@autoclosure,以表示这个参数可以是一个自动闭包的简化写法,这时就可以将该函数当做接受String类型参数的函数来呼叫。这个类型前面标注@autoclosure的参数会将自己转换成一个闭包,如下:

// 这时 customersInLine 为 ["Eddie", "Zack", "Kevin"]

// 这个函数的参数类型前面标注了 @autoclosure 
// 表示这参数可以是一个自动闭包的简化写法
func serve(customer customerProvider: @autoclosure () -> String) {
    
    
    print("开始移除 \(customerProvider()) !")
}

// 因为函数的参数类型有标注 @autoclosure 这个参数可以不用大括号 {}
// 而仅仅只需要[移除第一个成员]这个表达式 而这个表达式会返回[被移除的成员的值]
serve(customer: customersInLine.remove(at: 0))

如果你想要自动闭包允许逃逸,则必须同时标注@autoclosure和@escaping,以下是一个例子:

// 这时 customersInLine 为 ["Zack", "Kevin"]

// 宣告另一个变量 为一个阵列 其内成员的类型为 () -> String
var customerProviders: [() -> String] = []

// 定义一个函数 参数类型前面标注 @autoclosure @escaping 
// 表示参数是一个可逃逸自动闭包
func collectCustomerProviders(
  _ customerProvider: @autoclosure @escaping () -> String) {
    
    
    // 函数内部的动作是将当做参数的这个闭包 再加入新的阵列中 
    // 因为可逃逸 所以不会出错
    customerProviders.append(customerProvider)
}

// 呼叫两次函数
// 会将 customersInLine 剩余的两个成员都移除并转加入新的阵列中
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))

// 打印出:获得了 2 个成员
print("获得了 \(customerProviders.count) 个成员")

// 最后将这两个成员也从新阵列中移除
for customerProvider in customerProviders {
    
    
    print("开始移除 \(customerProvider()) !")
}
// 依序打印出:
// 开始移除 Zack !
// 开始移除 Kevin !

看明白了吗?

估计看完有点矇吧…

理解难度
★★★★★
实用程度
★☆☆☆☆

没关系多看几次。但是也不用花太多的时间卡在这里。这里的理解难度高,而实用程度低,表示闭包代码有其他的取代方式。当下能够写出自己看得懂的代码,比用了一段代码却不知道为啥还来得重要。

也许你永远也不会用上闭包!何必因这个混淆的篇章就打坏了你的学习兴致呢?大可不必。

高效阅读-事半功倍读书法-重点笔记-不长,都是干货

Guess you like

Origin blog.csdn.net/weixin_42385177/article/details/121594761